{'r'}; ($title, $body) = ("", ""); my $err = sub { my @err = @_; $title = $ML{'Error'}; $body = LJ::bad_input(@err); return; }; my $dbh = LJ::get_db_writer(); my $remote = LJ::get_remote(); my $cart = $POST{'cart'}; LJ::load_user_props($remote, 'allow_pay') if $remote; # if coming from the /store/ area, pick up their cart cookie for 'em, # save the /store/ code a bit of work (and redundancy) if ($POST{'action:addinventory'}) { $cart ||= $COOKIE{'cart'}; } my $cartobj = LJ::Pay::load_cart($cart); # make sure they own this cart if ($cartobj && $cartobj->{'userid'}) { return "You must be logged in as this cart's owner to proceed with checkout." unless $remote; return "You do not own this cart. If you'd like to start a new cart, please click " . "here." unless $cartobj->{'userid'} == $remote->{'userid'}; } my $ensurecart = sub { return if $cartobj; $cartobj = LJ::Pay::new_cart($remote); $cart = "$cartobj->{'payid'}-$cartobj->{'anum'}"; }; # don't let them modify the cart if they've already started # any sort of checkout process my $can_mod = ! $cartobj || ! $cartobj->{'method'}; # new order if ($POST{'action:neworder'}) { delete $BML::COOKIE{'cart'}; return BML::redirect("/pay/?c=new&old=$cart"); } # no cart... senseless action if (($POST{'action:checkout'} || $POST{'action:removesel'}) && ! $cartobj) { return BML::redirect("/pay/"); } # remove item(s) from cart if ($POST{'action:removesel'}) { return $err->("Can't modify checked-out cart.") unless $can_mod; my @remove; # piids to be removed, based on %POST submission my %extra_remove; # piid => userid my %paid_in_cart; # userid => unixtime when earliest paid account will apply my %bonus_in_cart; # userid => unixtime when earliest bonus feature will apply my %userids_in_cart; # userid => 1, hash for fast lookups foreach my $it (@{$cartobj->{'items'}}) { # the item they are trying to remove if ($POST{"del_$it->{'piid'}"}) { push @remove, $it; next; } my $ga = $it->{'giveafter'} || 0; my $uid = $it->{'rcptid'}; $userids_in_cart{$uid} = 1; # bonus feature if (LJ::Pay::is_bonus($it)) { # find which instance of this bonus feature takes effect first if (! defined $bonus_in_cart{$uid} || $ga < $bonus_in_cart{$uid}) { $bonus_in_cart{$uid} = $ga; } # paid account } elsif ($it->{'item'} eq "paidacct") { # find which paid account takes effect sooner if (! defined $paid_in_cart{$uid} || $ga < $paid_in_cart{$uid}) { $paid_in_cart{$uid} = $ga; } } } if (%bonus_in_cart) { # get expiration dates of paid accounts for users who have bonus in this cart my %paid_in_db; my $in = join(",", map { $dbh->quote($_) } keys %userids_in_cart); my $sth = $dbh->prepare("SELECT userid, UNIX_TIMESTAMP(paiduntil) AS 'paiduntil' " . "FROM paiduser WHERE userid IN ($in)"); $sth->execute(); $paid_in_db{$_->{'userid'}} = $_->{'paiduntil'} while $_ = $sth->fetchrow_hashref; # remove free users with no paid time in their cart foreach my $it (@{$cartobj->{'items'}}) { next if $POST{"del_$it->{'piid'}"}; # skip item if being deleted next unless LJ::Pay::is_bonus($it); my $ga = $it->{'giveafter'} || 0; my $uid = $it->{'rcptid'}; if ((! defined $paid_in_cart{$uid} || ($paid_in_cart{$uid} > 0 && $ga < $paid_in_cart{$uid})) && (! defined $paid_in_db{$uid} || $ga > $paid_in_db{$uid})) { $extra_remove{$it->{'piid'}} = $uid; } } # extra items were removed, notify the user why if (%extra_remove) { $body .= ""; $body .= ""; $body .= "
Pay with PayPal"; } return; } return BML::redirect("$LJ::SSLROOT/pay/cc.bml?c=$cart"); } if ($POST{'paymeth'} eq "free") { $dbh->do("UPDATE payments SET mailed='N', used='N', method='free', daterecv=NOW() ". "WHERE payid=? AND mailed='C' AND used='C'", undef, $cartobj->{'payid'}); $title = "Success"; $body = "Order placed. For reference, your order number is $cart."; return; } } my $msg_already_out = "Your currently active order has already been checked out. To add items you must first start a new order."; # add coupon to cart if ($POST{'action:coupon'}) { return $err->($msg_already_out) unless $can_mod; return $err->("Can't add a coupon to an empty order. Add items first, then apply your coupon.") unless $cartobj; my $coupon = $POST{'coupon'}; return $err->("Invalid coupon format") unless $coupon =~ /^(\d+)-(.+)$/; my ($cpid, $auth) = ($1, $2); my $cp = $dbh->selectrow_hashref("SELECT * FROM coupon WHERE cpid=? AND auth=?", undef, $cpid, $auth); return $err->("Invalid coupon") unless $cp; return $err->("Coupon belongs to somebody else") if $cp->{'locked'} && (! $remote || $remote->{'userid'} != $cp->{'rcptid'}); # see if coupon is already in this cart if (grep { $_->{'item'} eq "coupon" && $_->{'tokenid'} == $cp->{'cpid'} } @{$cartobj->{'items'}}) { return BML::redirect("/pay/?c=$cart"); } # can only add freeclothingitem coupons to carts with clothes in them if ($cp->{'type'} eq "freeclothingitem") { return $err->("There does not exist a clothing item for which this coupon can be used.") unless grep { $_->{'item'} eq "clothes"} @{$cartobj->{'items'}}; } # coupon is already attached to a cart if ($cp->{'payid'}) { # error generically if they're not logged in return $err->("Coupon has already been used") unless $remote; # see who owns the cart the coupon is attached to my $oldcart = $dbh->selectrow_hashref("SELECT userid, used, method, anum " . "FROM payments WHERE payid=?", undef, $cp->{'payid'}); # can't give a link to a cart they don't own return $err->("Coupon has already been used by another user") unless $remote->{'userid'} == $oldcart->{'userid'}; # now we know they own the cart, so we'll give them a link to view/modify it. # modify is only possible if 'used' = 'C' and 'method' = '' my $cart = "$cp->{'payid'}-$oldcart->{'anum'}"; my $verb = ($oldcart->{'used'} eq 'C' && ! $oldcart->{'method'}) ? "modify" : "view"; return $err->("The coupon you have entered has already been used on another one of your carts. " . "If you'd like, you can $verb the existing cart or " . "go back and enter a new coupon for the current cart."); } my $item = { 'item' => 'coupon', 'subitem' => $cp->{'type'}, 'tokenid' => $cpid, 'token' => $coupon, 'amt' => -$cp->{'arg'}, 'rcptid' => 0, }; $item = LJ::Pay::add_cart_item($cartobj, $item); return $err->("Error adding coupon to order: " . LJ::last_error()) unless $item; # mark coupon as used by this cart $dbh->do("UPDATE coupon SET payid=? WHERE cpid=?", undef, $cartobj->{'payid'}, $cp->{'cpid'}); return BML::redirect("/pay/?c=$cart"); } # add item to cart if ($POST{'action:additem'}) { return $err->($msg_already_out) unless $can_mod; $ensurecart->(); # itemname-subitem?-qty, subitem is optional my ($itemname, $subitem, $qty) = split(/\-/, $POST{'item'}); unless ($qty) { $qty = $subitem; undef $subitem; } my $good_bonus = sub { return 0 unless LJ::Pay::is_bonus($itemname); my $brec = $LJ::Pay::bonus{$itemname}; if (LJ::Pay::is_bonus($itemname, 'sized')) { return exists $brec->{items}->{$subitem} && exists $brec->{items}->{$subitem}->{qty}->{$qty}; } return exists $brec->{items}->{$qty}; }; # valid item? my $bad_item = 1; $bad_item = 0 if $good_bonus->(); # bonus features $bad_item = 0 if $itemname eq "paidacct" && defined $LJ::Pay::account{$qty}; $bad_item = 0 if $itemname eq "coupon" && grep { $_ eq $qty } @LJ::Pay::coupon; $bad_item = 0 if $itemname eq "perm" && $LJ::PERM_SALE; $bad_item = 0 if $itemname eq "coppa"; $bad_item = 0 if $itemname eq "rename"; return BML::redirect("/pay/?c=$cart") if $bad_item; my $item = { 'item' => $itemname, 'rcptid' => 0, }; ### Decide pricing/qty information depending on item type # paid account if ($itemname eq "paidacct") { $item->{'qty'} = $qty; $item->{'amt'} = $LJ::Pay::account{$qty}->{'amount'}; } # sized bonus feature elsif (LJ::Pay::is_bonus($itemname, 'sized')) { # essentially the subitem for sized bonus features is the 'size', but to make sure that # multiple orders don't get comp'd time, we'll store the previous size/qty as well, so # the actual stored subitem will be: # new_size-prev_exp-prev_size my ($prev_exp, $prev_size) = LJ::Pay::get_bonus_dim($remote, $itemname); $item->{'subitem'} = "$subitem-$prev_exp-$prev_size"; $item->{'amt'} = LJ::Pay::get_sized_bonus_price($remote, $cartobj, $itemname, $subitem, $qty); $item->{'qty'} = $qty; } # bool bonus feature elsif (LJ::Pay::is_bonus($itemname, 'bool')) { $item->{'qty'} = $qty; $item->{'amt'} = LJ::Pay::get_bool_bonus_price($itemname, $qty); } # coupons elsif ($itemname eq "coupon") { $item->{'amt'} = $qty; $item->{'subitem'} = "dollaroff"; } # rename token elsif ($itemname eq "rename") { $item->{'amt'} = 15; } # permanent account elsif ($itemname eq "perm") { $item->{'amt'} = 150; } # coppa verification elsif ($itemname eq "coppa") { $item->{'amt'} = 0; } ### Decide recipient if ($POST{'redemail'} && $POST{'for'} ne "email") { return $err->("Redemption email entered, but not selected. Not sure what you meant."); } my $rcptuser = {}; if ($POST{'for'} eq "remote") { return $err->("No longer logged in, but paying for logged in account") unless $remote; return $err->("Can't buy a coupon for yourself") if $itemname eq 'coupon'; $item->{'rcptid'} = $remote->{'userid'}; $rcptuser = $remote; } elsif ($POST{'for'} eq "other") { my $ou = LJ::load_user($POST{'otheruser'}); return $err->("Invalid gift recipient username") unless $ou; return $err->("Cannot buy paid features for syndicated accounts") if $ou->{'journaltype'} eq 'Y'; return $err->("Cannot buy paid features for renamed accounts") if $ou->{'journaltype'} eq 'R'; return $err->("Cannot buy paid features for " . LJ::ljuser($ou) . " due to account status") unless $ou->{'statusvis'} eq 'V' || $ou->{'statusvis'} eq 'M'; $item->{'rcptid'} = $ou->{'userid'}; $rcptuser = $ou; # don't let them easily check to see if they're blocked unless ($remote && $remote->{'user'} eq $ou->{'user'}) { my $pay_user = $remote ? $remote->{'user'} : 'anonymous'; my $pay_userid = $remote ? $remote->{'userid'} : undef; if (LJ::sysban_check('pay_user', $ou->{'user'})) { LJ::sysban_note($pay_userid, "Gift blocked based on user", { 'item' => $itemname, 'pay_user' => $pay_user, 'rcpt_user' => $ou->{'user'}, 'cart' => $cart }); return $err->("You are not allowed to buy gifts for " . LJ::ljuser($ou->{'user'})); } } } elsif ($POST{'for'} eq "email") { # can't buy bonus features for an email address return $err->("Extra features can only be purchased for existing users.") if LJ::Pay::is_bonus($itemname); my @email_errors; LJ::check_email($POST{'redemail'}, \@email_errors); return $err->(@email_errors) if @email_errors; $item->{'rcptemail'} = $POST{'redemail'}; # don't let them easily check to see if they're blocked unless ($remote && $remote->{'email'} eq $item->{'rcptemail'}) { my $pay_user = $remote ? $remote->{'user'} : 'anonymous'; my $pay_userid = $remote ? $remote->{'userid'} : undef; if (LJ::sysban_check('pay_email', $item->{'rcptemail'})) { LJ::sysban_note($pay_userid, "Gift blocked based on email", { 'item' => $itemname, 'pay_user' => $pay_user, 'rcpt_email' => $item->{'rcptemail'}, 'cart' => $cart }); return $err->("You are not allowed to buy gifts for '$item->{'rcptemail'}'"); } } } unless ($item->{'rcptid'} || $item->{'rcptemail'}) { return $err->("No specified recipient for item"); } # must be logged in and purchasing 'sized' bonus features for self if (LJ::Pay::is_bonus($itemname, 'sized')) { my $dispname = LJ::Pay::product_name($item, "short"); # must be logged in return $err->("Must be logged in to buy $dispname.") unless $remote; # can't buy as a gift return $err->("Can't buy $dispname as a gift.") unless $remote->{'userid'} == $item->{'rcptid'}; # shouldn't get this far, did they spoof the form values? return $err->("Cannot buy $dispname.") unless LJ::Pay::can_apply_sized_bonus($rcptuser, $cartobj, $item->{'item'}, $item->{'subitem'}, $item->{'qty'}); } # can't buy paid time for permanent accounts if ($item->{'rcptid'} && ($itemname eq 'paidacct' || $itemname eq 'perm')) { return $err->("Cannot buy paid time for permanent accounts") if $rcptuser->{'caps'} & 1 << $LJ::Pay::capinf{'perm'}->{'bit'}; } # can't have more than one permanent account per recipient if ($itemname eq 'perm') { foreach (@{$cartobj->{items}}) { return $err->("Cannot purchase more than one permanent account for a given recipient.") if $item->{rcptid} && $item->{rcptid} == $_->{rcptid} || $item->{rcptemail} && $item->{rcptemail} eq $_->{rcptemail}; } } # can't do coppa verifications except on $remote if ($itemname eq 'coppa') { return $err->("Cannot perform age verification on non-logged-in users") if ! $remote || $remote->{userid} != $item->{rcptid}; return $err->("Age verification not required for " . LJ::ljuser($remote)) unless $remote->underage; } $item->{'anon'} = $POST{'anongift'} ? 1 : 0; # delayed gift if ($POST{'giveafter'}) { return $err->("Cannot specify a Delivery Date on orders for yourself.") if $item->{'rcptid'} && $remote && $item->{'rcptid'} == $remote->{'userid'}; return $err->($ML{'/paidaccounts/usepaypal.bml.delivery.badformat'}) unless ($POST{'giveafter'} =~ /^(\d\d\d\d)\-(\d\d)\-(\d\d)(?: (\d\d):(\d\d))?/); my $dtime = 0; eval { $dtime = Time::Local::timegm(0, $5, $4, $3, $2-1, $1); }; return $err->($ML{'/paidaccounts/usepaypal.bml.delivery.badformat'}) if @$; my $delay = $dtime - time(); return $err->($ML{'/paidaccounts/usepaypal.bml.delivery.past'}) if $delay < 0; return $err->($ML{'/paidaccounts/usepaypal.bml.delivery.toofuture'}) if $delay > 60*60*24*31; $item->{'giveafter'} = $dtime; } else { $item->{'giveafter'} = undef; } # buying bonus-feature, lots of extra checks if (LJ::Pay::is_bonus($itemname) && ! $POST{'action:additem:confirm'}) { # check to see if the user is trying to buy bonus features that extend # past the expiration of their paid account... factor in any paid time # currently in the cart as well. if so, ask for confirmation my $cart_bonus_months = $item->{'qty'}; # months of bonus feat. in cart my $cart_paid_months = 0; # months of paidacct in cart my $cart_bonus_start = $item->{'giveafter'}; # start of bonus feature in cart my $cart_paid_start = undef; # start of paid account in cart my $cart_perm_acct = 0; # can't buy bool bonus features for perms foreach my $it (@{$cartobj->{'items'}}) { next unless $it->{'rcptid'} == $item->{'rcptid'}; my $ga = $it->{'giveafter'} || 0; # bonus item if ($it->{'item'} eq $itemname) { $cart_bonus_months += $it->{'qty'}; # find which instance of this bonus feature takes effect first if (! defined $cart_bonus_start || $ga < $cart_bonus_start) { $cart_bonus_start = $ga; } # paid account } elsif ($it->{'item'} eq "paidacct") { $cart_paid_months += $it->{'qty'}; # find which paid account takes effect sooner if (! defined $cart_paid_start || $ga < $cart_paid_start) { $cart_paid_start = $ga; } } elsif ($it->{'item'} eq 'perm') { $cart_perm_acct = 1; } } $cart_paid_start ||= 0; my $is_perm = ($cart_perm_acct || $rcptuser->{'caps'} & 1 << $LJ::Pay::capinf{'perm'}->{'bit'}); # give error if trying to buy bonus feature for permanent account # -- but we do allow buying disk quota for permanent accounts if (LJ::Pay::is_bonus($itemname, 'bool') && $is_perm) { my $dispname = LJ::Pay::product_name($item, "short"); return $err->("You cannot buy $dispname for permanent accounts."); } # return max of arguments, undef if none my $max = sub { @_ ? (sort { $b <=> $a } @_)[0] : undef; }; my $db_paid_time = $is_perm ? $LJ::EndOfTime : $dbh->selectrow_array("SELECT UNIX_TIMESTAMP(paiduntil) " . "FROM paiduser WHERE userid=?", undef, $item->{'rcptid'}) || 0; # used in messages, $rcptuser is always defined here my $utag = LJ::ljuser($rcptuser); # no paid account at all my $dispname = LJ::Pay::product_name($item, "short"); unless ($db_paid_time || $cart_paid_months) { return $err->("You can only buy extra features for an existing " . "paid account. If you would like to buy $dispname for " . "$utag, put a paid account for them to your cart and try again."); } my $ga = $item->{'giveafter'} || 0; # fix bonus giveafter to coincide with earliest paid account if ($cart_paid_start > 0 && (! $ga && ! $db_paid_time || $ga > $db_paid_time && $ga < $cart_paid_start)) { # can't adjust giveafter if recipient == remote if ($remote && $remote->{'userid'} == $item->{'rcptid'}) { return $err->("You are attempting to buy $dispname, but your paid " . "account will expire before the first paid account " . "currently in your cart will take effect. Since $dispname " . "cannot be applied to a free account, it cannot be added to your cart."); } # cart_paid_start is a localtime, convert it to gmtime in mysql format # to post back to ourselves $item->{'giveafter'} = LJ::mysql_time(Time::Local::timelocal(gmtime($cart_paid_start))); $POST{'giveafter'} = $item->{'giveafter'}; # pass on post variables $body .= ""; return; } # remove giveafter if no paid account in cart and it is after the # expiration of their current paid account if ($db_paid_time > 0 && $ga > $db_paid_time && (! $cart_paid_months || $ga < $cart_paid_start)) { # get rid of giveafter date in memory, to be clean delete $item->{'giveafter'}; delete $POST{'giveafter'}; # pass on post variables $body .= ""; return; } my $db_bonus_time = $max->($dbh->selectrow_array("SELECT UNIX_TIMESTAMP(), " . "UNIX_TIMESTAMP(expdate), " . "UNIX_TIMESTAMP(NOW() + INTERVAL daysleft DAY) " . "FROM paidexp WHERE userid=? AND item=?", undef, $item->{'rcptid'}, $itemname)); $db_bonus_time ||= 0; # the following check doesn't produce a warning in the case that there # is paid time set to be applied at a later date, but the bonus feature # being added is starting during the small amount of paid time still in # the db which will expire before the delayed paid time will be applied. # |--- won't catch if 2 months of bonus applied here # V # [DBTIME...[NOW] 2 DAYS] ... [NON-P-TIME] ... [2 MONTHS CART-P-TIME DELAYED] # since this is just a warning mechanism, we won't kill ourselves trying # to catch this situation # we add $cart_paid_months to the $paid_base to determine if it is after # when the bonus features will expire. if so, no error. my $paid_base = $db_paid_time || "NOW()"; # paid time in cart if ($cart_paid_start && $cart_paid_months) { # paid base is the max of when the cart paid time starts # and when the db paid time ends, since cart paid time # only applies once db paid time ends. $paid_base = $max->($cart_paid_start, $db_paid_time); } # we add $cart_bonus_months to the $bonus_base to determine if it is after # when the paid account will expire. if so, error. my $bonus_base = $db_bonus_time || "NOW()"; # bonus time in cart if ($cart_bonus_start && $cart_bonus_months) { # bonus base is the max of when the cart bonus time starts, # and when the db bonus time ends, since the delivery date # of the item currently being added is already figured in. $bonus_base = $max->($cart_bonus_start, $db_bonus_time); } my $bonus_base_stamp = LJ::mysql_time($bonus_base); my $paid_base_stamp = LJ::mysql_time($paid_base); # is bonus expiration before or on the same day as paid expiration? my $allow = $dbh->selectrow_array ("SELECT TO_DAYS(? + INTERVAL ? MONTH) <= TO_DAYS(? + INTERVAL ? MONTH)", undef, $bonus_base_stamp, $cart_bonus_months, $paid_base_stamp, $cart_paid_months); unless ($allow) { # pass on post variables $body .= ""; return; } } # add the item LJ::Pay::add_cart_item($cartobj, $item); return BML::redirect("/pay/?c=$cart"); } # add a physical item if ($POST{'action:addinventory'}) { my $prod = $POST{'product'}; return $err->("Invalid product.") unless $prod =~ /^(\w+?)-(.+)$/; return $err->($msg_already_out) unless $can_mod; my ($item_type, $subitem) = ($1, $2); # they need to be logged in (no gift products for now at least) return $err->("You need to log in before adding items to your cart.") unless $remote; # see if it's available, and its price my $inv = $dbh->selectrow_hashref("SELECT qty, avail, price FROM inventory WHERE ". "item=? AND subitem=?", undef, $item_type, $subitem); return $err->("Invalid product.") unless $inv; return $err->("Sorry, selected product is out of stock.") unless $inv->{'avail'}; my $item = { 'item' => $item_type, 'subitem' => $subitem, 'qty' => 1, 'amt' => $inv->{'price'}, 'rcptid' => $remote->{'userid'}, }; $ensurecart->(); my $rt = LJ::Pay::add_cart_item($cartobj, $item); return "Failed to add item to cart: " . $dbh->errstr unless $rt; return BML::redirect("/pay/?c=$cart"); } } _code?> body=> page?>