#!/usr/bin/perl # # LiveJournal.com-specific library # # This file is NOT licensed under the GPL. As with everything in the # "ljcom" CVS repository, this file is the property of Danga # Interactive and is made available to the public only as a reference # as to the best way to modify/extend the base LiveJournal server code # (which is licensed under the GPL). # # Feel free to read and learn from things in "ljcom", but don't use it verbatim # because we don't want your site looking like LiveJournal.com (our logo # and site scheme are our identity and we don't want to confuse users) # and we're sick of getting everybody's payment notifications when # they use our payment system without any modifications. # package LJ::Pay; use strict; use lib "$ENV{LJHOME}/cgi-bin"; use vars qw(%account %bonus %capinf @coupon %product %color %size %extra_sms %shopitem); use Carp qw(croak); use Time::Local (); # used by /paidaccounts/usepaypal.bml, at least. use POSIX (); # need to use LJ::Pay::Payment explicitly because it swaps in transaction configs use LJ::Pay::Payment; use Class::Autouse qw( LWP LWP::UserAgent DateTime LJ::EventLogRecord::PaymentStatusChanged LJ::VGift LJ::Pay::ShopVGift LJ::Pay::ShopVGiftSponsored LJ::Pay::Payment LJ::Pay::Payment::PayItem LJ::Pay::LoyaltyUserpic LJ::Pay::RecBill LJ::Pay::RecBill::Card LJ::Pay::RecBill::Log LJ::Pay::RecBill::Item LJ::Pay::RecBill::PendTime LJ::Blob ); require "ljlib.pl" unless $LJ::_LJLIB_INIT; require "accountcodes.pl"; require "phonepost.pl"; # hard-coded ljcom cap info %capinf = ( 'new' => { 'name' => 'New User' }, 'free' => { 'name' => 'Basic Account' }, 'early' => { 'name' => 'Early Adopter' }, 'paid' => { 'name' => 'Paid User' }, 'perm' => { 'name' => 'Permanent Account' }, 'plus' => { 'name' => 'Plus Account' }, ); # NOTE: the %capinf structure is deprecated. the following line # is for compatibility. don't use %capinf in new code. foreach (keys %capinf) { my $bitnum = LJ::class_bit($_); $capinf{$_}{bit} = $bitnum; die "undefined bit for $_" unless defined $bitnum; } # NOTE: Be sure to confirm the values in LJ::Pay::paid_account_price() if you change the values here! %account = ( 1 => { 'name' => 'pay.account.xmonths', 'amount' => undef, 'amount_rec' => 3 }, 2 => { 'name' => 'pay.account.xmonths', 'amount' => 5, 'amount_rec' => 5 }, 6 => { 'name' => 'pay.account.xmonths', 'amount' => 15, 'amount_rec' => 15 }, 12 => { 'name' => 'pay.account.xmonths', 'amount' => 25, 'amount_rec' => 19.95 }, ); # Dollar amounts for shop items %shopitem = ( 'rename' => 15, 'perm' => 175, ); # list of dollar amount @coupon = (5, 15, 20, 25); # bonus features are of 2 types: # - "bool" are either on or off (userpics), 'cap' key is required # - "sized" have a magnitude associated with them (how much disk quota) %bonus = ( # userpics are a 'bool' item 'userpic' => { 'name' => 'pay.product.extrauserpics', 'type' => 'bool', 'cap' => 9, # cap bit to activate for user 'items' => { # quantities # * undef amount means recurring only 1 => { 'name' => '1 month', 'amount' => undef, 'amount_rec' => 1, }, 2 => { 'name' => '2 months', 'amount' => 2, 'amount_rec' => 2, }, 6 => { 'name' => '6 months', 'amount' => 6, 'amount_rec' => 6, }, 12 => { 'name' => '12 months', 'amount' => 10, 'amount_rec' => 10, } } }, # disk quota is a 'sized' item 'diskquota' => { 'name' => 'pay.product.extrastorage', 'type' => 'sized', 'cap' => undef, # optional 'apply_hook' => \&LJ::Pay::diskquota_apply_hook, 'items' => { # 1024*10 = 10240 = 10GB 10240 => { 'name' => '10 GB', 'qty' => { 1 => { 'name' => '1 months', 'amount' => undef, 'amount_rec' => 3, }, 2 => { 'name' => '2 months', 'amount' => 5, 'amount_rec' => 5, }, 6 => { 'name' => '6 months', 'amount' => 14, 'amount_rec' => 14, }, 12 => { 'name' => '12 months', 'amount' => 24, 'amount_rec' => 24, }, } }, } } ); # now allow a mechanism for individual bonus items to be disabled foreach my $itemname (keys %bonus) { next unless $LJ::DISABLED{"bonus-$itemname"}; delete $bonus{$itemname}; } %extra_sms = ( 100 => "4.99", 250 => "9.99", ); %product = ( "clothes-short" => [ "Short-Sleeved Shirt", [ qw(white black grey orange bluedusk leaf )]], "clothes-long" => [ "Long-Sleeved Shirt", [ qw(white black grey navyblue )]], "clothes-polo" => [ "Embroidered Polo Shirt", [ qw(white black grey navyblue )]], "clothes-babydoll" => [ "\"Baby Doll\" Fitted Shirt", [ qw(white black grey pink royalblue )]], "clothes-hooded" => [ "Hooded Sweatshirt", [ qw(black navyblue) ]], "clothes-twillhat" => [ "Stonewashed Cap", [ qw(khaki black navyblue) ]], "clothes-skullcap" => [ "Skull Cap", [ qw(grey black) ], ], "clothes-travelmug" => [ "Stainless Steel Coffee Mug", [ qw(silver) ], ], ); %color = ( 'white' => "pay.color.white", 'black' => "pay.color.black", 'grey' => "pay.color.grey", 'navyblue' => "pay.color.navyblue", 'royalblue' => "pay.color.royalblue", 'bluedusk' => "pay.color.bluedusk", 'pink' => "pay.color.pink", 'leaf' => "pay.color.leaf", 'orange' => "pay.color.orange", 'khaki' => "pay.color.khaki", 'silver' => "pay.color.silver", ); %size = ( 'os' => [0, "pay.size.all"], 's' => [1, "pay.size.small"], 'm' => [2, "pay.size.medium"], 'l' => [3, "pay.size.large"], 'xl' => [4, "pay.size.xlarge"], 'xxl' => [5, "pay.size.xxlarge"], '3xl' => [6, "pay.size.3xlarge"], '4xl' => [7, "pay.size.4xlarge"], ); ## ## Gateway for processing SMS payments ## -- almost identical to LJ::sms_gateway, except with a price ## point specified to facilitate the charge ## sub sms_payment_gateway { my $price_point = shift; croak "invalid price point: $price_point" unless exists $LJ::SMS_PAYMENT_CONFIG{$price_point}; my $class = "DSMS::Gateway" . ($LJ::SMS_GATEWAY_TYPE ? "::$LJ::SMS_GATEWAY_TYPE" : ""); eval "use $class"; die "unable to use $class: $@" if $@; return $class->new(config => $LJ::SMS_PAYMENT_CONFIG{$price_point}); } sub extra_sms_price { my $qty = shift; croak "invalid extra_sms qty: $qty" unless exists $LJ::Pay::extra_sms{$qty}; return $LJ::Pay::extra_sms{$qty}; } sub new_sms_payment { my $u = shift; croak "invalid user object for extra_sms payment" unless LJ::isu($u); my $qty = shift; croak "invalid extra_sms qty: $qty" unless exists $LJ::Pay::extra_sms{$qty}; my $price = LJ::Pay::extra_sms_price($qty); die "unable to find price for sms qty: $qty" unless defined $price; my $pmt = LJ::Pay::Payment->new_memonly ( forwhat => 'cart', userid => $u->id, anum => int(rand()*65536), method => 'sms', used => 'C', mailed => 'C', datesent => undef, # default to NOW() ) or die "unable to instantiate new payment"; $pmt->add_item ( item => 'sms-quota', subitem => $qty, qty => 1, amt => $price, rcptid => $u->{userid}, ) or die "unable to add sms-quota item"; $pmt->save or die "unable to save payment"; return $pmt; } ## hook called from create.bml after an account is made sub post_create { my $o = shift; my $userid = $o->{'userid'}; my $user = $o->{'user'}; my $dbh = LJ::get_db_writer(); return unless $o->{'code'}; my ($acid, $auth) = LJ::acct_code_decode($o->{'code'}); return unless $acid; # check to see if this account was created using an # acid that was created as the result of a payment. # in other words, we might now need to make the # account paid. # old table my $payid = $dbh->selectrow_array("SELECT payid FROM acctpay WHERE acid=$acid"); if ($payid) { # trust that paid users gave valid email address (so email alias then works immediately) LJ::update_user($userid, { status => 'A' }); # now that userid != 0, they'll be mailed and setup # with a minute if the cronjob is running. $dbh->do("UPDATE payments SET userid=$userid WHERE payid=$payid"); return; } # new table my $piid = $dbh->selectrow_array("SELECT piid FROM acctpayitem WHERE acid=$acid"); if ($piid) { # trust that paid users gave valid email address (so email alias then works immediately) LJ::update_user($userid, { status => 'A' }); # do the payment immediately my ($item, $qty) = $dbh->selectrow_array("SELECT item, qty FROM payitems ". "WHERE piid=$piid"); my $mo = $item eq "paidacct" ? $qty : 0; $mo = 99 if $item eq "perm"; LJ::Pay::add_paid_months($userid, $mo); return; } } sub diskquota_apply_hook { my ($u, $item) = @_; # RIP old business model: this is where used to enqueue a # white-labeled-parent-site ping to update what a user's quota # should be. for instance: deadjournal pinging LJ where LJ was # hosting all of DJ's photo hosting under DJ's style. DJ ran this # on their beta site for a bit, but we never went into business # with it. (around time of SixApart acquistion, and 6a wasn't # interested in FotoBilder code/services). now this code is # removed because we're killing cmdbuffer usage now that all # cmdbuffer is moving to using our new job queue system. } # uuid: userid # what: paidaccount, etc ('what' db field) # trans: 'P' => pay, 'X' => expire sub update_paytrans { my ($uuid, $what, $chflag) = @_; my $uid = LJ::want_userid($uuid); return undef unless $uid && $what && $chflag; my $dbh = LJ::get_db_writer() or return undef; # load all transitions for this user my @trans = @{ $dbh->selectall_arrayref ("SELECT time, action FROM paytrans WHERE userid=? AND what=?", undef, $uid, $what) || [] }; # now trans => ([ time, action ], ...) my $time = time(); my $action = ''; # currently paidaccount is the only 'what' if ($what eq 'paidaccount') { my $renew_thresh = 14; # days between 'renew' and 'return' # evpent definitions: # * 'new' - user has never paid for an account before, this is # their first payment # * 'ext' - user extended their account while it was still active # * 'renew' - user paid before, expired, then re-purchased within # $renew_thresh days of expiration # * 'return' - user paid before, expired, then re-purchased after # $renew thresh days of expiration # * 'expire' - user had paid account expire # adding paid months to account if ($chflag eq 'P') { # if this is the first purchase we've seen, then their account is new if (! @trans) { $action = 'new'; # if we've seen purchases before, we must look at their last expiration # to see if this should be considered a 'return' or a 'renew' } else { # find last expiration/pay actions a user had my $lexp = 0; my $lpay = 0; foreach my $tr (@trans) { $lexp = $tr->[0] if $tr->[1] eq 'expire' && $tr->[0] > $lexp; $lpay = $tr->[0] if $tr->[1] ne 'expire' && $tr->[0] > $lpay; } $action = 'ext' if $lpay && (! $lexp || $lexp < $lpay && $time > $lpay); $action ||= $lexp && $lexp < ($time - 86400 * $renew_thresh) ? 'return' : 'renew'; } # expiring an existing paid account } elsif ($chflag eq 'X') { $action = 'expire'; } } # insert transition into db $dbh->do("INSERT INTO paytrans VALUES (?,?,?,?)", undef, $uid, $time, $what, $action) or return undef; return 1; } sub add_paid_time { my ($argu, $time, $bonus_added) = @_; # or 99 months for perm my $userid = LJ::want_userid($argu); return undef unless $userid && (! $bonus_added || ref $bonus_added); # figure out the amount of time, as well # as what type of units it is measured in $time = ref $time ? $time : [ $time, 'month' ]; my ($timeval, $units) = @$time; return undef unless $timeval > 0; $units = lc($units); $units ||= "month"; return undef unless $units =~ /^(?:month|day|second|epoch)$/; my $dbh = LJ::get_db_writer() or return undef; my $is_perm = $timeval == 99 && $units eq 'month'; my $u = ref $argu ? $argu : undef; # permanent account if ($is_perm) { # add permanent and paid caps $u = LJ::modify_caps($userid, ['paid', 'perm'],[]) or return undef; # update caps of passed object if (ref $argu) { $argu->{caps} = $u->{caps} unless $argu->{caps} == $u->{caps}; } # create paiduser row $dbh->do("INSERT IGNORE INTO paiduser (userid) VALUES (?)", undef, $userid); # remove from recbill LJ::Pay::remove_recbill($u, { 'nonotify' => 1 }); # regular } else { # add paid cap $u = LJ::modify_caps($userid, ['paid'], []) or return undef; # update caps of passed object if (ref $argu) { $argu->{caps} = $u->{caps} unless $argu->{caps} == $u->{caps}; } my $add_sql; my $qtv = $dbh->quote($timeval); if ($units eq 'epoch') { # explicit expiration time $add_sql = "FROM_UNIXTIME($qtv)"; } else { $add_sql = "DATE_ADD(NOW(), INTERVAL $qtv $units)"; } $dbh->do("INSERT INTO paiduser (userid, paiduntil) VALUES (?, $add_sql)", undef, $userid); if ($dbh->err) { $add_sql = "DATE_ADD(GREATEST(IFNULL(paiduntil, NOW()), NOW()), INTERVAL $qtv $units)" unless $units eq 'epoch'; # already an paying member; renewing: $dbh->do("UPDATE paiduser SET paiduntil=$add_sql" . "WHERE userid=?", undef, $userid) or return undef; } } # at this point the paid time has been applied. any failure could cause the # caller to retry us later and cause paid time to be applied twice. # log this paid account activation my $logmsg = "adding paid ${units}s: " . ($is_perm ? "perm" : $timeval); $logmsg = "setting paidacct exp time: " . LJ::mysql_time($timeval) if $units eq 'epoch'; LJ::statushistory_add($userid, undef, 'pay_modify', $logmsg); $LJ::CACHE_PAIDGROUP ||= LJ::get_userid("paidmembers"); if ($u->{'journaltype'} eq "P" && $LJ::CACHE_PAIDGROUP) { # add as friend to paidmembers group (if it exists on this server) LJ::add_friend($LJ::CACHE_PAIDGROUP, $userid); } # add email alias $u->update_email_alias; # note the transition for stats LJ::Pay::update_paytrans($userid, 'paidaccount', 'P') or return undef; # FIXME: If the bonus-activation operation fails, then any # pending bonus items won't be applied to the account being # given paid time. Further, if we return undef from here on # failure, callers such as bin/maint/pay.pl could retry us # endlessly, adding time each time the above code is executed, # then dying when trying to add the bonus features. We need # to queue up the activation action on failure and return true # add any extra bonus feature time that needs to be added @$bonus_added = LJ::Pay::activate_frozen_bonus($userid); return 1; } sub get_account_exp { my $u = shift; my $userid = LJ::want_userid($u); return undef unless $userid; my $dbh = LJ::get_db_writer() or return undef; return $dbh->selectrow_array ("SELECT UNIX_TIMESTAMP(paiduntil) FROM paiduser WHERE userid=?", undef, $userid) } sub get_bonus_exp { my ($u, @items) = @_; return undef unless @items && ! grep { ! LJ::Pay::is_bonus($_) } @items; my $userid = LJ::want_userid($u); return undef unless $userid; my $dbh = LJ::get_db_writer() or return undef; my $bind = join(',', map { "?" } @items); my $sth = $dbh->prepare ("SELECT item, UNIX_TIMESTAMP(expdate) FROM paidexp WHERE userid=? AND item IN ($bind)"); $sth->execute($userid, @items); my %ret = (); while (my ($it, $exp) = $sth->fetchrow_array) { $ret{$it} = $exp; } return \%ret } sub add_paid_months { &nodb; my ($userid, $months, $bonus_added) = @_; # or 99 months for perm return LJ::Pay::add_paid_time(@_); } sub remove_paid_months { &nodb; my ($userid, $months, $it) = @_; return LJ::Pay::remove_paid_time(@_); } sub remove_paid_time { my ($userid, $time, $it, $opts) = @_; # or 99 months for perm $userid += 0; return undef unless $userid && $time >= 0; return 1 unless $time; # figure out the amount of time, as well # as what type of units it is measured in $time = ref $time ? $time : [ $time, 'month' ]; my ($timeval, $units) = @$time; return undef unless $timeval > 0; $units = lc($units); $units ||= "month"; return undef unless $units =~ /^(?:month|day|second|epoch)$/; my $dbh = LJ::get_db_writer() or return undef; # find account status before changes are made so we can report in the log # later. FIXME: this should use APIs my $pre = $dbh->selectrow_hashref("SELECT u.caps, p.paiduntil FROM user u LEFT JOIN paiduser p ". "ON p.userid=u.userid WHERE u.userid=?", undef, $userid); # 99 months means we're working on a permanent account my $is_perm = $timeval == 99 && $units eq 'month'; my $sub_sql; my $qtv = $dbh->quote($timeval); if ($units eq 'epoch') { # explicit expiration time $sub_sql = "FROM_UNIXTIME($qtv)"; } else { $sub_sql = "DATE_SUB(paiduntil, INTERVAL $qtv $units)"; } # subtract $months from paid time, unless perm # -- need to lower paiduntil even from permanent accounts so that in the # case of account transfer, they will not have a balance remaining to # transfer again later. $dbh->do("UPDATE paiduser SET paiduntil=$sub_sql ". "WHERE userid=?", undef, $userid); # remove them from being a paid user if their time has run out # -- permanent accounts are immune from deactivation due to paiduntil # being in the past unless ($is_perm) { my $timeleft = $dbh->selectrow_array ("SELECT paiduntil > NOW() FROM paiduser WHERE userid=?", undef, $userid); if (!$timeleft && !$opts->{'keep_paid_acct'}) { LJ::Pay::remove_paid_account($userid, undef, $is_perm); } } # log this change to statushistory my $post = $dbh->selectrow_hashref ("SELECT u.caps, p.paiduntil FROM user u LEFT JOIN paiduser p ". "ON p.userid=u.userid WHERE u.userid=?", undef, $userid); my $logmsg = "removing paid ${units}s: " . ($is_perm ? "perm" : $timeval); $logmsg = "setting paidacct exp time: " . LJ::mysql_time($timeval) if $units eq 'epoch'; my $extra = $it ? " payment: $it->{'payid'}\[$it->{'piid'}]" : ""; LJ::statushistory_add($userid, undef, "revoke", "item=paidacct; $timeval ${units}s; was: caps $pre->{caps}/$pre->{paiduntil}, ". "now: $post->{caps}/$post->{paiduntil}$extra"); return 1; } sub acct_code_from_payid { &nodb; my $payid = shift; $payid += 0; my $dbh = LJ::get_db_writer(); my $sth; $dbh->do("LOCK TABLES acctpay WRITE, acctcode WRITE"); # does one already exist? $sth = $dbh->prepare("SELECT acctcode.acid, acctcode.auth FROM acctcode, acctpay ". "WHERE acctcode.acid=acctpay.acid AND acctpay.payid=$payid"); $sth->execute; my ($acid, $auth) = $sth->fetchrow_array; if ($acid) { $dbh->do("UNLOCK TABLES"); return LJ::acct_code_encode($acid, $auth); } # if not, let's add one. my $code = LJ::acct_code_generate(0); if ($code) { ($acid, $auth) = LJ::acct_code_decode($code); $dbh->do("REPLACE INTO acctpay (payid, acid) VALUES ($payid, $acid)"); } $dbh->do("UNLOCK TABLES"); return $code; } sub new_rename_token { &nodb; my $payid = shift; my $dbh = LJ::get_db_writer(); my $code = LJ::rand_chars(10); $dbh->do("INSERT INTO renames (token, payid) VALUES (?, ?)", undef, $code, $payid) or return undef; my $renid = $dbh->{'mysql_insertid'} or return undef; my $token = sprintf("%06x%s", $renid, $code); return wantarray() ? ($token, $renid) : $token; } sub register_payment { &nodb; my $o = shift; my $sth; my $error = $o->{'error'}; my $zuid = $o->{'zerouserid'}; my $userid = 0; my $user = "???"; unless ($zuid) { $user = lc($o->{'user'}); $user =~ s/\W//g; $userid = LJ::get_userid($user); unless ($userid) { $$error = "Invalid user ($user)"; return 0; } } my $dbh = LJ::get_db_writer(); my $out_payid = $o->{'out_payid'}; my $qdatesent = $dbh->quote($o->{'datesent'}); my $qamount = $dbh->quote($o->{'amount'}+0); my $qmonths = $dbh->quote($o->{'months'}+0); my $qnotes = $dbh->quote($o->{'notes'}); my $qmethod = $dbh->quote($o->{'method'}); my $qwhat = $dbh->quote($o->{'what'}); my $qgiveafter = $dbh->quote($o->{'giveafter'}); my $payid; my $digest = Digest::MD5::md5_hex($o->{'unique_id'}); # prevent duplicates (quite common from paypal -> pp_notify.bml) if ($o->{'unique_id'}) { $dbh->do("LOCK TABLES payments WRITE, duplock WRITE"); $sth = $dbh->prepare("SELECT dupid FROM duplock WHERE realm='payments' AND reid=0 AND ". "userid=$userid AND digest='$digest'"); $sth->execute; ($payid) = $sth->fetchrow_array; if ($payid) { $dbh->do("UNLOCK TABLES"); $$out_payid = $payid; return $userid; } } my ($mailed, $used) = ("N", "N"); $mailed = "Y" if $o->{'never_mail'}; $used = "Y" if $o->{'never_use'}; ### now, insert a payment $sth = $dbh->prepare("INSERT INTO payments (userid, datesent, daterecv, amount, months, used, mailed, notes, method, forwhat, giveafter) ". "VALUES ($userid, $qdatesent, NOW(), $qamount, $qmonths, '$used', '$mailed', $qnotes, $qmethod, $qwhat, $qgiveafter)"); $sth->execute; if ($dbh->err) { $$error = "Database error: " . $dbh->errstr; $dbh->do("UNLOCK TABLES"); return 0; } $payid = $sth->{'mysql_insertid'}; if ($o->{'unique_id'}) { $dbh->do("INSERT INTO duplock (realm, reid, userid, digest, dupid, instime) ". "VALUES ('payments', 0, $userid, '$digest', $payid, NOW())"); $dbh->do("UNLOCK TABLES"); } ### insert payment search values if ($o->{'search'}) { my $s = $o->{'search'}; foreach my $k (keys %$s) { my $v = $s->{$k}; my $vals = ref $v eq "ARRAY" ? $v : [ $v ]; foreach (@$vals) { $dbh->do("INSERT INTO paymentsearch (payid, ikey, ival) VALUES ($payid, ?, ?)", undef, $k, $_); } } } my $whoenter = $o->{'remote'}->{'user'} || "auto"; my $msgbody = "Entered by $whoenter: payment# $payid for $user\n\n"; $msgbody .= "AMOUNT: $o->{'amount'} MONTHS: $o->{'months'}\n"; $msgbody .= "METHOD: $o->{'method'} WHAT: $o->{'what'}\n"; $msgbody .= "DATE: $o->{'datesent'}\n"; $msgbody .= "NOTES:\n$o->{'notes'}\n"; LJ::send_mail({ 'to' => 'paypal@livejournal.com', 'from' => 'lj_noreply@livejournal.com', 'charset' => 'utf-8', 'subject' => "Payment \#$payid -- $user", 'body' => $msgbody, }); $$out_payid = $payid; return $userid; } sub paypal_parse_custom { my $custom_str = shift; my %custom; foreach my $pair (split(/&/, $custom_str)) { my ($key, $value) = split(/=/, $pair); foreach (\$key, \$value) { tr/+/ /; s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; } $custom{$key} = $value; } return \%custom; } sub register_paypal_payment { &nodb; my $pp = shift; my $o = shift; my $error = $o->{'error'}; my %custom = %{ LJ::Pay::paypal_parse_custom($pp->{custom}) || {}}; # for some reason, every few weeks a payment comes in without the # 'newacct' parameter. so this hack adds it. some broken browser # out there? $custom{'newacct'} = 1 if ($custom{'months'} && ! defined $custom{'user'}); # cart support (new payment system) if ($custom{'cart'}) { my $dbh = LJ::get_db_writer(); my $cartobj = LJ::Pay::load_cart($custom{'cart'}); unless ($cartobj) { $$error = "Invalid cart"; return 0; } if ($cartobj->{'mailed'} ne "C") { # cart is already paid for? or paypal is being # dumb (as usual) and sending a dup notification, # so let's see if the txn matches from previous my $old_txn = $dbh->selectrow_array("SELECT ival FROM paymentsearch ". "WHERE payid=? AND ikey='pptxnid'", undef, $cartobj->{'payid'}); # tell paypal we're cool if this is a dup return 1 if $old_txn && $old_txn eq $pp->{'txn_id'}; $$error = "Cart is already paid for"; return 0; } unless ($cartobj->{'amount'} * 100 == $pp->{'payment_gross'} * 100) { $$error = "Payment gross ($pp->{'payment_gross'} doesn't match cart price ($cartobj->{'amount'})"; return 0; } my $s = { 'ppemail' => $pp->{'payer_email'}, 'pptxnid' => $pp->{'txn_id'}, 'pplastname' => $pp->{'last_name'}, }; foreach my $k (keys %$s) { $dbh->do("INSERT INTO paymentsearch (payid, ikey, ival) VALUES (?, ?, ?)", undef, $cartobj->{'payid'}, $k, $s->{$k}); } $dbh->do("UPDATE payments SET mailed='N', used='N', ". " method='paypal', daterecv=NOW() ". "WHERE payid=? AND mailed='C'", undef, $cartobj->{'payid'}); if ($dbh->err) { $$error = "Database error"; return 0; } return 1; } # old payment system my $price = LJ::Pay::paid_account_price( interval => $custom{months}, rec => 0 ); unless (($price == $pp->{'payment_gross'}) || ($custom{'months'} == 99 && $pp->{'payment_gross'} == 100) || ($custom{'what'} eq "rename" && $pp->{'payment_gross'} == 15)) { $$error = "Payment gross not valid for that month value"; return 0; } my %mon2num = qw(Jan 1 Feb 2 Mar 3 Apr 4 May 5 Jun 6 Jul 7 Aug 8 Sep 9 Oct 10 Nov 11 Dec 12); my $pp_to_sql_date = sub { my $ppdate = shift; if ($ppdate =~ /\b(\w\w\w) (\d{1,2}), (\d\d\d\d)\b/) { my ($year, $month, $day); $year = $3; $month = $mon2num{$1}; $day = $2; return sprintf("%04d-%02d-%02d", $year, $month, $day); } return ""; }; # is this for a new account? need to generate an account code # and mail it to the user. if ($custom{'newacct'}) { my %pay; my $payid = 0; my @emails = ($pp->{'payer_email'}); if ($custom{'email'} && $custom{'email'} ne $pp->{'payer_email'}) { push @emails, $custom{'email'}; } $pay{'zerouserid'} = 1; # no userid yet. new acccount. $pay{'datesent'} = $pp_to_sql_date->($pp->{'payment_date'}); $pay{'method'} = "paypal"; $pay{'notes'} = "PayPal Transaction ID: " . $pp->{'txn_id'} . "\n"; $pay{'what'} = "account"; # one of (account, rename, gift) $pay{'unique_id'} = $pp->{'txn_id'} . $pp->{'payment_status'}; $pay{'error'} = $error; $pay{'out_payid'} = \$payid; $pay{'search'} = { 'ppemail' => \@emails, 'pptxnid' => $pp->{'txn_id'}, 'pplastname' => $pp->{'last_name'}, }; $pay{'notes'} .= "Payment Status: $pp->{'payment_status'}\n"; if ($pp->{'payment_status'} eq "Completed") { $pay{'amount'} = $pp->{'payment_gross'}; $pay{'months'} = $custom{'months'}; register_payment(\%pay); if ($payid) { my $code = acct_code_from_payid($payid); unless ($code) { return 0; } LJ::send_mail({ 'to' => join(", ", @emails), 'from' => $LJ::ACCOUNTS_EMAIL, 'charset' => 'utf-8', 'subject' => 'Account code', 'body' => "Here is your account creation code you can use to start setting up your journal:\n\n $code\n\nOr, just click or copy/paste this:\n\n $LJ::SITEROOT/create.bml?code=$code\n", }); return 1; } return 0; } # any non-complete payment we enter is just a placeholder for # paymentsearch indexes to point back to. $pay{'never_mail'} = $pay{'never_use'} = 1; if ($pp->{'payment_status'} eq "Pending") { $pay{'notes'} .= "Pending Reason: $pp->{'pending_reason'}\n"; $pay{'notes'} .= "\nLiveJournal has been notified of your payment. If you paid with an eCheck, your account code will be emailed to you when the check clears.\n"; } register_payment(\%pay); return 0 unless $payid; LJ::send_mail({ 'to' => $pp->{'payer_email'}, 'from' => $LJ::ACCOUNTS_EMAIL, 'charset' => 'utf-8', 'subject' => "LiveJournal Payment Info ($payid)", 'body' => "PayPal has notified LiveJournal of your payment. We've logged this transaction as \#$payid. Its current status is shown below:\n\n$pay{'notes'}\n", }); return 1; } # handle incomplete payments for the general case (when we know username # of buyer) if ($pp->{'payment_status'} ne "Completed") { my %pay; $pay{'user'} = $custom{'user'}; $pay{'method'} = "paypal"; $pay{'unique_id'} = $pp->{'txn_id'} . $pp->{'payment_status'}; $pay{'datesent'} = $pp_to_sql_date->($pp->{'payment_date'}); $pay{'notes'} = "PayPal Transaction ID: " . $pp->{'txn_id'} . "\n"; $pay{'notes'} .= "Payment Status: $pp->{'payment_status'}\n"; $pay{'error'} = $error; $pay{'search'} = { 'ppemail' => $pp->{'payer_email'}, 'pptxnid' => $pp->{'txn_id'}, 'pplastname' => $pp->{'last_name'}, }; $pay{'never_use'} = 1; # but we do mail them. if ($pp->{'payment_status'} eq "Pending") { $pay{'notes'} .= "Pending Reason: $pp->{'pending_reason'}\n\n"; $pay{'notes'} .= "You will get another email when this payment clears (or fails)."; } return 1 if register_payment(\%pay); return 0; } # not a gift. if ($custom{'for'} eq "") { my %pay; $pay{'user'} = $custom{'user'}; $pay{'months'} = $custom{'months'}+0; $pay{'amount'} = $pp->{'payment_gross'}; $pay{'datesent'} = $pp_to_sql_date->($pp->{'payment_date'}); $pay{'what'} = $custom{'what'} eq "rename" ? "rename" : "account"; # one of (account, rename, gift) $pay{'method'} = "paypal"; $pay{'notes'} = "PayPal Transaction ID: " . $pp->{'txn_id'}; $pay{'unique_id'} = $pp->{'txn_id'}; $pay{'error'} = $error; $pay{'search'} = { 'ppemail' => $pp->{'payer_email'}, 'pptxnid' => $pp->{'txn_id'}, 'pplastname' => $pp->{'last_name'}, }; if (register_payment(\%pay)) { return 1; } return 0; } # gift: process one payment for buyer and one for recipient. if ($custom{'for'}) { my $giftfor = $custom{'for'}; my $buyer_ret; my $recipient_ret; my %pay; ## buyer's reciept. %pay = (); $pay{'user'} = $custom{'user'}; $pay{'months'} = $LJ::GIVER_BONUS{$custom{'months'}}+0; # no months for buyer, unless specified $pay{'amount'} = $pp->{'payment_gross'}; $pay{'datesent'} = $pp_to_sql_date->($pp->{'payment_date'}); $pay{'what'} = "gift"; # one of (account, rename, gift) $pay{'method'} = "paypal"; $pay{'notes'} = "PayPal Transaction ID: " . $pp->{'txn_id'} . "\nGift for $giftfor."; $pay{'unique_id'} = $pp->{'txn_id'} . "BUYER"; # must be unique (see below) $pay{'error'} = $error; $pay{'search'} = { 'ppemail' => $pp->{'payer_email'}, 'pptxnid' => $pp->{'txn_id'}, 'pplastname' => $pp->{'last_name'}, }; $buyer_ret = register_payment(\%pay); ## recipient's reciept %pay = (); $pay{'giveafter'} = $custom{'giveafter'}; $pay{'user'} = $giftfor; $pay{'months'} = $custom{'months'}; $pay{'amount'} = 0; # recipient didn't pay $pay{'datesent'} = $pp_to_sql_date->($pp->{'payment_date'}); $pay{'what'} = "account"; # one of (account, rename, gift) $pay{'method'} = "paypal"; my $fromwho = $custom{'anon'} ? "(anonymous user)" : $custom{'user'}; $pay{'notes'} = "PayPal Transaction ID: " . $pp->{'txn_id'} . "\nGift from: $fromwho."; $pay{'unique_id'} = $pp->{'txn_id'} . "RCPT"; # must be unique (see above) $pay{'error'} = $error; $pay{'search'} = { 'ppemail' => $pp->{'payer_email'}, 'pptxnid' => $pp->{'txn_id'}, 'pplastname' => $pp->{'last_name'}, }; $recipient_ret = register_payment(\%pay); ## did they both succeed? return ($buyer_ret && $recipient_ret); } } sub verify_paypal_transaction { my $hash = shift; my $opts = shift; my $ua = LJ::get_useragent( role => 'pay_paypal', timeout => 6, ); $ua->agent("LJ-PayPalAuth/0.1"); # Create a request my @urls = ("$LJ::PAYPAL_URL?cmd=_notify-validate"); foreach my $url (@urls) { my $req = new HTTP::Request POST => $url; $req->content_type('application/x-www-form-urlencoded'); $req->content(join("&", map { LJ::eurl($_) . "=" . LJ::eurl($hash->{$_}) } keys %$hash)); # Pass request to the user agent and get a response back my $res = $ua->request($req); # Check the outcome of the response if ($res->is_success) { if ($res->content eq "VERIFIED") { return 1; } ${$opts->{'error'}} = "Invalid"; return 0; } } ${$opts->{'error'}} = "Connection Problem"; return 0; } sub LJ::Pay::load_paidexp_items { my $u = shift; return undef unless LJ::isu($u); my $dbh = LJ::get_db_writer() or return undef; my @ret = (); my $sth = $dbh->prepare("SELECT * FROM paidexp WHERE userid=?"); $sth->execute($u->{userid}); return undef if $dbh->err; push @ret, $_ while $_ = $sth->fetchrow_hashref; return @ret; } sub LJ::Pay::load_cart { my $cart = shift; return undef unless $cart =~ /^(\d+)-(\d+)$/; my ($payid, $anum) = ($1, $2); my $cartobj = LJ::Pay::Payment->load ( payid => $payid, anum => $anum, forwhat => 'cart' ) or return undef; return $cartobj; } sub LJ::Pay::new_cart { my $remote = shift; my $cartobj = LJ::Pay::Payment->new ( forwhat => 'cart', userid => $remote ? $remote->{userid} : 0, anum => int(rand()*65536), used => 'C', mailed => 'C', datesent => undef, # default to NOW() ) or return undef; LJ::Pay::payvar_append($cartobj, "creator_ip", LJ::get_remote_ip()); return $cartobj; } sub LJ::Pay::payvar_add { my $cartobj = shift; return undef unless $cartobj; return $cartobj->payvar_add(@_); } sub LJ::Pay::payvar_append { my $cartobj = shift; return undef unless $cartobj; return $cartobj->payvar_append(@_); } sub LJ::Pay::payvar_set { my $cartobj = shift; return undef unless $cartobj; return $cartobj->payvar_append(@_); } sub LJ::Pay::user_has_used_method { my ($u, $method) = @_; croak "invalid user: $u" unless LJ::isu($u); my $dbr = LJ::get_db_reader(); my $payid = $dbr->selectrow_array ("SELECT payid FROM payments WHERE userid=? AND method=? AND used='Y' LIMIT 1", undef, $u->userid, $method); return $payid ? 1 : 0; } sub LJ::Pay::payid_set_state { my ($payid, $ctry, $st) = @_; return undef unless $payid; $ctry ||= "??"; $st ||= "??"; my $str = $ctry; $str .= "-$st" if $ctry eq 'US'; # if we don't know the state, we insert a literal "??" into the db my $dbh = LJ::get_db_writer(); return $dbh->do("REPLACE INTO paystates (payid, state) VALUES (?,?)", undef, $payid, $str); } sub LJ::Pay::check_country_state { my ($ctry, $st, $err) = @_; $ctry = uc($ctry); $st = uc($st); my (%country, %state); LJ::load_codes({ country => \%country, # "us" => "United States" state => \%state }); # validate given country unless ($country{$ctry}) { while (my ($key, $val) = each %country) { next unless $ctry eq uc($val); # "UNITED STATES" eq "UNITED STATES" $ctry = uc($key); # "US" } } unless ($country{$ctry}) { $$err = "Invalid country: $ctry" if $ctry; return (undef, undef); } # don't handle non-US states right now return ($ctry, undef) unless $ctry eq 'US'; # now, did they specify a state code or state name? $st = uc(LJ::trim($st)); # full state name specified, get state code from that unless ($state{$st}) { while (my ($key, $val) = each %state) { next unless $st eq uc($val); # "OHIO" eq "OHIO" $st = uc($key); # "US" } } unless ($state{$st}) { $$err = "Invalid US state: $st" if $st; return ($ctry, undef); } # now $st should be a state code return ($ctry, $st); } sub LJ::Pay::add_cart_item { my $cartobj = shift; my $args = shift; return LJ::error("no cart") unless $cartobj; my $it = $cartobj->add_item ( status => $args->{$_} || 'cart', map { $_ => $args->{$_} } qw(item subitem qty rcptid amt rcptemail anon giveafter token tokenid), ) or return LJ::error($@); return $it; } sub LJ::Pay::remove_cart_items { my $cartobj = shift; my @items = @_; return 0 unless $cartobj; return 1 unless @items; return $cartobj->remove_items(@items); } sub LJ::Pay::update_shipping_cost { my ($cartobj, $country) = @_; return 0 unless $cartobj; return $cartobj->update_shipping_cost; } ## ## Shop items can be ## - tangible (e.g. clothes) ## - intangible (vgifts, paid accounts, disk quota etc) ## - coupons. ## By buying tangible items, money leaves LJ inc. ## ## Gift Certificates (coupons) can be ## - intangible, suitable for virtual items only. These certs are given in promotion actions. ## - tangible (very rare type, suitable for tangible items only) ## - free, suitable for anything. These certs are bought in shop for real money only. ## - freeclothingitem (obsolete?) ## ## Coupons can't be bought by coupons, to prevent conversion of restricted coupon into free. ## Charity vgifts are tangible, since money for them leaves LJ. ## sub LJ::Pay::is_tangible { my $it = shift; return 1 if $it->{'item'} eq 'clothes'; return 1 if $it->{'item'} eq 'coupon'; if ($it->{item} eq 'vgift') { my $vg = LJ::Pay::ShopVGift->new($it->{subitem}); return ($vg->is_charity) ? 1 : 0; } return 0; } # see LJ::Pay::Payment::coupon_reduce for notes # on what is returned from this function sub LJ::Pay::coupon_reduce { my $cartobj = shift; return undef unless $cartobj; return $cartobj->coupon_reduce; } sub LJ::Pay::send_coupon_email { my ($u, $token, $amt, $type) = @_; return undef unless $u && $token && defined $amt; my $email = ref $u ? $u->email_raw : $u; return undef unless $email; my $inttxt; if ($type eq 'int') { $inttxt .= "This gift certificate is only valid for intangible items such as paid accounts "; $inttxt .= "and bonus features. It cannot be used to buy other gift certificate or to "; $inttxt .= "buy clothing.\n\n"; } elsif ($type eq 'tan') { $inttxt .= "This gift certificate is only valid for tangible items such as tee shirts and "; $inttxt .= "hoodies. It cannot be used to buy other gift certificates or intangible items "; $inttxt .= "such as paid accounts.\n\n"; } # print dollars my $damt = sub { sprintf("\$%.02f", shift()) }; LJ::send_mail({ 'to' => $email, 'from' => $LJ::ACCOUNTS_EMAIL, 'fromname' => $LJ::SITENAMESHORT, 'wrap' => 1, 'charset' => 'utf-8', 'subject' => "You've Received an $LJ::SITENAMEABBREV Gift Certificate", 'body' => "$LJ::SITENAMESHORT gift certificate code:\n\n". " $token\n\n". # possibly a notice saying this is an intangible coupon $inttxt . "You can redeem it for " . $damt->($amt) . " USD in $LJ::SITENAMESHORT services:\n" . " $LJ::SITEROOT/shop/\n\n" . "NOTE: Your gift certificate is only valid for one use, so be sure that your order's " . "value is greater than or equal to " . $damt->($amt) . " USD.\n\n" . "Enjoy!\n\n" . "$LJ::SITENAMESHORT Team\n$LJ::SITEROOT/", }); return 1; } # somewhat generic function, but for now it just sets allow_pay once we have received a # valid payment from a user... so they don't run into open proxy + etc restrictions later sub LJ::Pay::note_payment_from_user { my $u = shift; return undef unless LJ::isu($u); # need to load the userprop unless it exists unless (exists $u->{allow_pay}) { LJ::load_user_props($u, 'allow_pay') or return undef;; } # nothing to do if allow_pay is already set return 1 if $u->{allow_pay} eq 'Y'; # set allow_pay on this user if necessary if (LJ::set_userprop($u, 'allow_pay', 'Y')) { # log to statushistory my $sys_id = LJ::get_userid('system'); LJ::statushistory_add($u, $sys_id, "allow_pay", "automatically allowing payments after successful transaction"); # successfully set return 1; } # error setting userprop above return undef; } sub LJ::Pay::send_fraud_email { my ($cartobj, $u) = @_; return undef unless $cartobj; # assure $u is valid with 'fraud_watch' loaded, # or undef if the cart has no rcptid if ($cartobj->{userid}) { $u ||= LJ::load_userid($cartobj->{userid}); LJ::load_user_props($u, 'fraud_watch') unless $u && exists $u->{fraud_watch}; } else { undef $u; } # find items in cart, then load userids for items which have rcptids my @items = @{$cartobj->{items}||[]}; my $ru = LJ::load_userids(map { $_->{rcptid} } grep { $_->{rcptid} } @items); # build array of fraud-watched recipient user objects and the items they are purchasing my @fraud_rcpt = (); foreach my $it (@items) { my $ruobj = $ru->{$it->{rcptid}} or next; LJ::load_user_props($ruobj, 'fraud_watch'); push @fraud_rcpt, [$it, $ruobj] if $ruobj->{fraud_watch}; } # if there's anything to mail, do it now if (my $u_watch = $u && $u->{fraud_watch} or @fraud_rcpt) { # if there are recipients on fraud watch, make a list of # their usernames and what they're trying to buy my $rcpt_txt = ""; if (@fraud_rcpt) { $rcpt_txt .= "Cart recipient information: (only users with active fraud watches)\n\n"; foreach (@fraud_rcpt) { my ($it, $fu) = @$_; $rcpt_txt .= " User: $fu->{user}\n"; $rcpt_txt .= " Item: " . LJ::Pay::product_name($it) . "\n\n"; } } LJ::send_mail({ 'to' => $LJ::ACCOUNTS_EMAIL, 'from' => $LJ::ACCOUNTS_EMAIL, 'wrap' => 1, 'charset' => 'utf-8', 'subject' => "Fraud alert: Payment #$cartobj->{payid}", 'body' => "This warning has been sent because a payment transaction has been " . "processed on $LJ::SITENAMESHORT. One or more of the users involved " . "with this payment are on a fraud watch.\n\n" . "For full information about this payment, see the link below:\n\n" . " $LJ::SITEROOT/admin/accounts/paiddetails.bml?payid=$cartobj->{payid}\n\n" . "Cart owner information:\n\n" . " User: " . ($u ? $u->{user} : $cartobj->{rcptemail}) . "\n" . " Watch: " . ($u_watch ? "yes" : "no") . "\n" . " Payid: $cartobj->{'payid'}\n" . " Time: " . LJ::mysql_time() . "\n\n" . $rcpt_txt, }) or return undef; } return 1; } sub LJ::Pay::update_cart_total { my $cartobj = shift; return 0 unless $cartobj; return $cartobj->update_total; } sub LJ::Pay::can_mod_cart { my $cartobj = shift; return 0 unless $cartobj; return $cartobj->can_modify; } sub LJ::Pay::can_checkout_cart { my $cartobj = shift; return 0 unless $cartobj; return $cartobj->can_checkout; } sub LJ::Pay::cart_contains_coppa { my $cartobj = shift; return 0 unless $cartobj; return $cartobj->contains_coppa; } sub LJ::Pay::cart_needs_shipping { my $cartobj = shift; return 0 unless $cartobj; return $cartobj->needs_shipping; } sub LJ::Pay::item_needs_shipping { my $it = shift; return 0 unless $it; return $it->needs_shipping; } sub LJ::Pay::reserve_items { my $cartobj = shift; my $out_list = shift; # listref to push out of stock product names onto die "Can't reserve items in undef cart.\n" unless $cartobj; my @prods = grep { $_->{'item'} eq "clothes" } @{$cartobj->{'items'}}; return 1 unless @prods; my $dbh = LJ::get_db_writer(); my %need; foreach my $pr (@prods) { next if $pr->{'qty_res'} >= $pr->{'qty'}; my $pkey = "$pr->{'item'}-$pr->{'subitem'}"; $need{$pkey}->{'count'} += $pr->{'qty'} - $pr->{'qty_res'}; push @{$need{$pkey}->{'items'}}, $pr; $need{$pkey}->{'item'} = $pr->{'item'}; $need{$pkey}->{'subitem'} = $pr->{'subitem'}; } foreach my $pr (keys %need) { my $n = $need{$pr}; my $avail = $dbh->selectrow_array("SELECT avail FROM inventory WHERE item=? AND subitem=?", undef, $n->{'item'}, $n->{'subitem'}); next if $avail >= $n->{'count'}; push @$out_list, LJ::Pay::product_name($n->{'item'}, $n->{'subitem'}); } # fail if items were out of stock return 0 if @$out_list; # reserve items if they're in stock (yes, this is racy, but that's # the least of the hellish inventory management problems) foreach my $pr (keys %need) { my $n = $need{$pr}; $dbh->do("UPDATE inventory SET avail=avail-? WHERE item=? AND subitem=?", undef, $n->{'count'}, $n->{'item'}, $n->{'subitem'}); foreach my $it (@{$n->{'items'}}) { $dbh->do("UPDATE payitems SET qty_res=qty WHERE piid=? AND payid=?", undef, $it->{'piid'}, $it->{'payid'}); } } return 1; } sub LJ::Pay::product_name { # @_: item, subitem, qty, short? my $item = shift; my ($subitem, $qty, $short) = @_; # case 1: $it, $short if (ref $item) { $subitem = $item->{'subitem'}; $qty = $item->{'qty'}; $item = $item->{'item'}; $short = shift; } # otherwise, case 2: $item, $subitem, $qty, $short? # now we should have all the right vars if ($item eq "clothes") { my ($type, $color, $size) = split(/-/, $subitem); return join(' ', LJ::Lang::ml($LJ::Pay::size{$size}->[1]), LJ::Lang::ml($LJ::Pay::color{$color}), $LJ::Pay::product{"clothes-$type"}->[0]); } if ($item eq "vgift") { my $vg = LJ::Pay::ShopVGift->new($subitem); return LJ::Lang::ml('pay.product.vgift') . ($short ? "" : " - " . LJ::Lang::ml($vg->display_name)); } if ($item eq "paidacct") { return LJ::Lang::ml('pay.product.paidacct') . ($short ? "" : " - " . LJ::Lang::ml($LJ::Pay::account{$qty}->{'name'}, {'num' => $qty})); } if ($item eq "perm") { return LJ::Lang::ml('pay.product.perm'); } if ($item eq "rename") { return LJ::Lang::ml('pay.product.rename'); } if ($item eq "coppa") { return LJ::Lang::ml('pay.product.coppa'); } if ($item eq "coupon") { my ($type) = split(/-/, $subitem); if ($type eq "freeclothingitem") { return LJ::Lang::ml('pay.product.giftcert.freeclothing'); } if ($type =~ /^dollaroff(int|tan)?/) { if ($1 && $1 eq 'tan') { return LJ::Lang::ml('pay.product.giftcert.tan'); } elsif ($1) { return LJ::Lang::ml('pay.product.giftcert.int'); } return LJ::Lang::ml('pay.product.giftcert'); } } if (LJ::Pay::is_bonus($item, 'bool')) { my $bitem = $LJ::Pay::bonus{$item}; return LJ::Lang::ml($bitem->{'name'}) . ($short ? "" : (" - " . ($bitem->{'items'}->{$qty}->{'name'} || $qty))); } if (LJ::Pay::is_bonus($item, 'sized')) { my $bitem = $LJ::Pay::bonus{$item}; my $size = (split("-", $subitem))[0]; my $sizeit = $bitem->{'items'}->{$size}; my $qtyit = $sizeit->{'qty'}->{$qty}; return ($sizeit->{'name'} || $size) . " " . LJ::Lang::ml($bitem->{'name'}) . ($short ? "" : (" - " . ($sizeit->{'qty'}->{$qty}->{'name'} || $qty))); } return "$item-$subitem"; } # whatever page loads this should be including # pay.css and pay.js sub cc_form { my ( $countries, $err, $POST, $card, $opts ) = @_; my $ret; $ret = ''; # init some vars my ($name, $exp_m, $exp_y, @exp_yrs); if ( $card ) { $name = $card->get_name_first() . " " . $card->get_name_last(); ($exp_y, $exp_m) = $card->get_exp(); } my $year = (gmtime())[5]; $year += 1900; push @exp_yrs, $year + $_ for (0..10); # card type $ret .= "'; # card # $ret .= "'; if ($opts->{cvv2}) { # card verification # $ret .= "'; } # card expiration $ret .= "'; # spacer $ret .= ''; # name on card $ret .= "'; my $countryval = $card ? $card->get_country : $POST->{country}; $ret .= "'; # address $ret .= "'; $ret .= "'; $ret .= "'; $ret .= "'; $ret .= "'; unless ($opts->{'has_coppa'}) { $ret .= ""; } $ret .= ''; $ret .= ''; if ($opts->{'has_coppa'}) { $ret .= '
" . LJ::Lang::ml('pay.cc.form.cardtype.label') . " "; $ret .= LJ::html_select( { name => 'type', selected => $POST->{type}, class => 'input_field', tabindex => 1 }, map { $_ => $_ } LJ::Pay::RecBill->valid_cards); $ret .= '
" . LJ::Lang::ml('pay.cc.form.cardnumber.label') . " "; $ret .= LJ::html_text( { name => 'cc_num', id => 'cc_num', value => $POST->{cc_num}, maxlength => 16, class => 'input_field', tabindex => 2 } ); $ret .= '
" . LJ::Lang::ml('pay.cc.form.cvv2.label') . " "; $ret .= LJ::html_text( { name => 'cc_cvv2', id => 'cc_cvv2', value => $POST->{cc_cvv2}, maxlength => 4, style => 'width: 40px', class => 'input_field', tabindex => 3 } ); $ret .= ' '; $ret .= LJ::Lang::ml('pay.cc.form.cvv2.info', {'aopts' => "href='$LJ::SSLROOT/pay/cvv2.bml' target='_blank'"}); $ret .= ''; $ret .= '
" . LJ::Lang::ml('pay.cc.form.cardexpires.label') . " "; $ret .= LJ::html_select( { name => 'exp_m', selected => $POST->{exp_m}, class => 'field', tabindex => 4 }, map { $_ => sprintf("%02d", $_) } (1..12) ) . " "; $ret .= LJ::html_select( { name => 'exp_y', selected => $POST->{exp_y}, class => 'field', tabindex => 5 }, map { $_ => $_ } @exp_yrs ) . " "; $ret .= '
 
" . LJ::Lang::ml('pay.cc.form.name.label') . " "; $ret .= LJ::html_text( { name => 'name_full', id => 'name_full', value => $name || $POST->{name_full}, maxlength => 80, class => 'input_field', tabindex => 6 } ); $ret .= '
" . LJ::Lang::ml('pay.cc.form.country.label') . " "; $ret .= LJ::html_select( { name => 'country', selected => ( $countryval || 'US' ), class => 'input_field', tabindex => 7 }, map { $_ => $countries->{$_} } sort { $countries->{$a} cmp $countries->{$b} } keys %$countries ); $ret .= '
" . LJ::Lang::ml('pay.cc.form.address.label') . " "; $ret .= LJ::html_text( { name => 'addr', id => 'addr', value => ( $card ? $card->get_addr() : $POST->{addr} ), maxlength => 50, class => 'input_field', tabindex => 8 } ); $ret .= '
" . LJ::Lang::ml('pay.cc.form.city.label') . " "; $ret .= LJ::html_text( { name => 'city', id => 'city', value => ( $card ? $card->get_city() : $POST->{city} ), maxlength => 20, class => 'input_field', tabindex => 9 } ); $ret .= '
" . LJ::Lang::ml('pay.cc.form.state.label') . " "; # load US and Canadian states my (%states); LJ::load_codes({ state => \%states }); $ret .= LJ::html_select( { name => 'state', selected => ( $card ? $card->get_state() : $POST->{state} || '' ), class => 'input_field', tabindex => 10 }, ('', LJ::Lang::ml('pay.cc.form.state.uscanadaonly')), map { $_ => $states{$_} } sort { $states{$a} cmp $states{$b} } keys %states ); # find a state value from a stored card or post value, unless # it is a known US state my $stateval = $card ? $card->get_state : $POST->{state_other}; $stateval = '' if $countryval =~ /^(US)$/ && $states{$stateval}; $ret .= "
" . LJ::Lang::ml('pay.cc.form.state.other') . " "; $ret .= LJ::html_text( { name => 'state_other', id => 'state_other', value => $stateval, maxlength => 64, class => 'input_field', style => 'width: 200px', } ); $ret .= '
" . LJ::Lang::ml('pay.cc.form.postalcode.label') . " "; $ret .= LJ::html_text( { name => 'postal_code', id => 'postal_code', value => ( $card ? $card->get_postal_code() : $POST->{postal_code} ), maxlength => 10, style => 'width: 80px', class => 'input_field', tabindex => 11 } ); $ret .= '
" . LJ::Lang::ml('pay.cc.form.email.label') . " "; $ret .= LJ::html_text( { name => 'email', id => 'email', value => ( $card ? $card->get_email() : $POST->{email} ), class => 'input_field', tabindex => 12 } ); $ret .= '
"; $ret .= "" . LJ::Lang::ml('pay.cc.form.note') . ""; $ret .= "
 
'; $ret .= $err; $ret .= '
'; $ret .= "
"; $ret .= LJ::html_submit( 'update', LJ::Lang::ml('pay.cc.form.btn.coppaconfirm'), { id => 'submit_cc', tabindex => 13, style => "margin-right: 50px;", } ); $ret .= "
"; } else { $ret .= ''; $ret .= " "; $ret .= LJ::html_submit( 'update', LJ::Lang::ml('pay.cc.form.btn.finish'), { id => 'submit_cc', class => 'btn', tabindex => 13, } ); $ret .= ''; } return $ret; } # Will also set state to value of state_other if non-US sub cc_form_validate { my ($POST, $err) = @_; unless ($POST->{type} =~ /^Visa|MasterCard|AmEx|Discover$/) { $$err = LJ::Lang::ml('pay.cc.validate.cardtype', {'cardtype' => $POST->{type}}); return undef; } my $cc_len = 16; my $cv_len = 3; if ($POST->{type} eq 'AmEx') { $cc_len = 15; $cv_len = 4; } unless ($POST->{cc_num} =~ /^\d{$cc_len}$/) { $$err = LJ::Lang::ml('pay.cc.validate.cardnumber', {'cardtype' => $POST->{type}, 'num' => $cc_len}); return undef; } unless ($POST->{cc_cvv2} =~ /^\d{$cv_len}$/) { $$err = LJ::Lang::ml('pay.cc.validate.cvv2', {'cardtype' => $POST->{type}, 'num' => $cv_len}); return undef; } my ($sec,$min,$hour,$mday,$mon,$year, $wday,$yday,$isdst) = localtime(); $mon++; $year += 1900; if ($POST->{exp_y} < $year || ($POST->{exp_y} == $year && $POST->{exp_m} < $mon)) { $$err = LJ::Lang::ml('pay.cc.validate.cardexpires'); return undef; } unless ($POST->{name_full} =~ /\S+\s+\S+/) { $$err = LJ::Lang::ml('pay.cc.validate.name'); return undef; } unless (length($POST->{addr})) { $$err = LJ::Lang::ml('pay.cc.validate.address'); return undef; } if ($POST->{country} eq 'US') { unless ($POST->{postal_code} =~ /^(\d{5})(-\d{4})?$/) { $$err = LJ::Lang::ml('pay.cc.validate.postalcode.us.invalid'); return undef; } if (int($1) == 0) { $$err = LJ::Lang::ml('pay.cc.validate.postalcode.us.zero'); return undef; } my (%states); LJ::load_codes({ "state" => \%states }); if (! $states{$POST->{state}}) { $$err = LJ::Lang::ml('pay.cc.validate.state.us'); return undef } } else { unless (length($POST->{postal_code}) > 0 && length($POST->{postal_code}) < 15) { $$err = LJ::Lang::ml('pay.cc.validate.postalcode.other'); return undef; } # Since we are not-US, they should be using the state # textfield instead. So overwrite the state key with # that value, but don't verify they entered anything # since some countries have nothing to enter. $POST->{state} = $POST->{state_other}; } my $countries = LJ::Pay::RecBill->valid_countries; unless ($countries->{$POST->{country}}) { $$err = LJ::Lang::ml('pay.cc.validate.country'); return undef; } my @email_errors; LJ::check_email($POST->{email}, \@email_errors); if ($POST->{email} && @email_errors) { $$err = join(", ", @email_errors); return undef; } return 1; } # FIXME: this should probably be in something like weblib-local.pl sub LJ::Pay::get_blurb { my $pos = shift; my $u = shift; # We don't really care if we get a real u, only sometimes my $dyk_upsell; if (LJ::is_enabled("paid_acct_sale")) { my $salepriceonetime = LJ::Pay::paid_account_price( interval => 12, rec => 0 ); my $normalpriceonetime = LJ::Pay::paid_account_price( interval => 12, rec => 0, get_normal_price => 1 ); $dyk_upsell = BML::ml('pay.blurb.dyk.autopay.signup.sale', { normalprice => "\$$normalpriceonetime", saleprice => "\$$salepriceonetime" }); } else { $dyk_upsell = BML::ml('pay.blurb.dyk.autopay.signup', { price => "\$19.95 USD" }); } my $giftshop; if (LJ::is_enabled("paid_acct_sale")) { $giftshop = BML::ml('pay.blurb.giftshop.text.sale', {'sitename' => $LJ::SITENAMESHORT, 'aopts' => "href='$LJ::SITEROOT/shop/view.bml?class=merchandise'"}) } else { $giftshop = BML::ml('pay.blurb.giftshop.text', {'sitename' => $LJ::SITENAMESHORT, 'aopts' => "href='$LJ::SITEROOT/shop/view.bml?class=merchandise'"}); } my @blurbs = ( # gift shop [ "", $giftshop ], # pay as you go [ "", BML::ml('pay.blurb.tryit.text', {'aopts1' => "href='$LJ::SITEROOT/shop/view.bml?item=paidaccount&gift=1'", 'aopts2' => "href='$LJ::SITEROOT/shop/view.bml?item=paidaccount'"}) ], # bonus features [ "", BML::ml('pay.blurb.bonus.text', {'aopts' => "href='$LJ::SITEROOT/shop/index.bml?class=bonusfeatures'"}) ], # Why auto pay [ "$BML::ML{'pay.blurb.whyautopay.header'}