2947 lines
104 KiB
Perl
2947 lines
104 KiB
Perl
#!/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.
|
|
#
|
|
|
|
require "$ENV{'LJHOME'}/cgi-bin/ljlib.pl";
|
|
|
|
package LJ::Pay;
|
|
|
|
use strict;
|
|
use vars qw(%account %bonus %capinf @coupon %product %color %size);
|
|
use Time::Local (); # used by /paidaccounts/usepaypal.bml, at least.
|
|
use LWP;
|
|
use LWP::UserAgent;
|
|
|
|
# hard-coded ljcom cap info
|
|
|
|
%capinf = (
|
|
'new' => { 'bit' => 0, 'name' => 'New User' },
|
|
'free' => { 'bit' => 1, 'name' => 'Free User' },
|
|
'early' => { 'bit' => 2, 'name' => 'Early Adopter' },
|
|
'paid' => { 'bit' => 3, 'name' => 'Paid User' },
|
|
'perm' => { 'bit' => 4, 'name' => 'Permanent Account' },
|
|
);
|
|
|
|
%account = (
|
|
2 => { 'name' => '2 months', 'amount' => 5 },
|
|
6 => { 'name' => '6 months', 'amount' => 15 },
|
|
12 => { 'name' => '12 months', 'amount' => 25 },
|
|
);
|
|
|
|
# list of dollar amount
|
|
@coupon = (5, 15, 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' => 'Extra Userpics',
|
|
'type' => 'bool',
|
|
'cap' => 9, # cap bit to activate for user
|
|
'items' => {
|
|
# quantities
|
|
2 => { 'name' => '2 months', 'amount' => 2 },
|
|
6 => { 'name' => '6 months', 'amount' => 6 },
|
|
12 => { 'name' => '12 months', 'amount' => 10 }
|
|
}
|
|
},
|
|
|
|
# disk quota is a 'sized' item
|
|
'diskquota' => {
|
|
'name' => 'Disk Quota',
|
|
'type' => 'sized',
|
|
'cap' => undef, # optional
|
|
'apply_hook' => \&LJ::Pay::diskquota_apply_hook,
|
|
'items' => {
|
|
# size => quantity (months)
|
|
# - prices are NOT determined. these are just made up numbers for testing.
|
|
250 => {
|
|
'name' => '250 MiB',
|
|
'qty' => {
|
|
2 => { 'name' => '2 months', 'amount' => 10 },
|
|
6 => { 'name' => '6 months', 'amount' => 20 },
|
|
12 => { 'name' => '12 months', 'amount' => 36 },
|
|
}
|
|
},
|
|
500 => {
|
|
'name' => '500 MiB',
|
|
'qty' => {
|
|
2 => { 'name' => '2 months', 'amount' => 20 },
|
|
6 => { 'name' => '6 months', 'amount' => 40 },
|
|
12 => { 'name' => '12 months', 'amount' => 72 },
|
|
}
|
|
},
|
|
1024 => {
|
|
'name' => '1024 MiB',
|
|
'qty' => {
|
|
2 => { 'name' => '2 months', 'amount' => 30 },
|
|
6 => { 'name' => '6 months', 'amount' => 60 },
|
|
12 => { 'name' => '12 months', 'amount' => 100 },
|
|
}
|
|
},
|
|
|
|
}
|
|
}
|
|
);
|
|
|
|
# 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};
|
|
}
|
|
|
|
%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(grey black) ], "disable_coupons"],
|
|
"clothes-twillhat" =>
|
|
[ "Stonewashed Cap", [ qw(khaki black navyblue) ]],
|
|
);
|
|
|
|
%color = (
|
|
'white' => "White",
|
|
'black' => "Black",
|
|
'grey' => "Grey",
|
|
'navyblue' => "Navy Blue",
|
|
'royalblue' => "Royal Blue",
|
|
'bluedusk' => "Blue Dusk",
|
|
'pink' => "Pink",
|
|
'leaf' => "Leaf Green",
|
|
'orange' => "Orange",
|
|
'khaki' => "Khaki",
|
|
);
|
|
|
|
%size = (
|
|
'os' => [0, "One Size Fits All"],
|
|
's' => [1, "Small"],
|
|
'm' => [2, "Medium"],
|
|
'l' => [3, "Large"],
|
|
'xl' => [4, "X-Large"],
|
|
'xxl' => [5, "XX-Large"],
|
|
'3xl' => [6, "3X-Large"],
|
|
'4xl' => [7, "4X-Large"],
|
|
);
|
|
|
|
## 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) = @_;
|
|
|
|
# we have successfully done nothing
|
|
return 1 unless $LJ::FB_QUOTA_NOTIFY;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
my ($size, $exptime) = $dbh->selectrow_array("SELECT size, UNIX_TIMESTAMP(expdate) " .
|
|
"FROM paidexp WHERE userid=? AND item=?",
|
|
undef, $u->{userid}, $item);
|
|
return undef unless $size && $exptime; # eh?
|
|
|
|
# add cmdbuffer job to send XML-RPC request to FotoBilder later
|
|
return LJ::cmd_buffer_add($u->{clusterid}, $u->{userid},
|
|
"pay_fb_xmlrpc",
|
|
{
|
|
item => $item,
|
|
size => $size,
|
|
exptime => $exptime,
|
|
});
|
|
}
|
|
|
|
# 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 ($userid, $time, $bonus_added) = @_; # or 99 months for perm
|
|
$userid += 0;
|
|
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 eq 'month' || $units eq 'day';
|
|
|
|
my $dbh = LJ::get_db_writer()
|
|
or return undef;
|
|
|
|
my $is_perm = $timeval == 99 && $units eq 'month';
|
|
|
|
# permanent account
|
|
if ($is_perm) {
|
|
|
|
# add permanent and paid caps
|
|
LJ::modify_caps($userid, ['paid', 'perm'],[])
|
|
or return undef;
|
|
|
|
# create paiduser row
|
|
$dbh->do("INSERT IGNORE INTO paiduser (userid) VALUES (?)", undef, $userid);
|
|
|
|
# regular
|
|
} else {
|
|
|
|
# add paid cap
|
|
LJ::modify_caps($userid, ['paid'], [])
|
|
or return undef;
|
|
|
|
$dbh->do("INSERT INTO paiduser (userid, paiduntil) VALUES (?, DATE_ADD(NOW(), INTERVAL ? $units))",
|
|
undef, $userid, $timeval);
|
|
if ($dbh->err) {
|
|
# already an paying member; renewing:
|
|
$dbh->do("UPDATE paiduser SET paiduntil=DATE_ADD(GREATEST(IFNULL(paiduntil, NOW()), NOW()), INTERVAL ? $units) " .
|
|
"WHERE userid=?", undef, $timeval, $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
|
|
LJ::statushistory_add($userid, undef, 'pay_modify',
|
|
"adding paid ${units}s: " . ($is_perm ? "perm" : $timeval));
|
|
|
|
$LJ::CACHE_PAIDGROUP ||= LJ::get_userid("paidmembers");
|
|
|
|
# get a fresh userid from the database
|
|
my $u = LJ::load_userid($userid, "force");
|
|
|
|
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);
|
|
}
|
|
|
|
LJ::load_user_props($u, 'no_mail_alias');
|
|
# add email alias, if account is validated
|
|
if ($u->{'status'} eq "A" &&
|
|
! $u->{'no_mail_alias'} &&
|
|
! exists $LJ::FIXED_ALIAS{$u->{'user'}}) {
|
|
$dbh->do("INSERT IGNORE INTO email_aliases (alias, rcpt) VALUES (?,?)",
|
|
undef, "$u->{'user'}\@$LJ::USER_DOMAIN", $u->{'email'});
|
|
}
|
|
|
|
# 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 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) = @_; # or 99 months for perm
|
|
$userid += 0;
|
|
return undef unless $userid && $months >= 0;
|
|
return 1 unless $months;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
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 = $months == 99;
|
|
|
|
# subtract $months from paid time, unless perm
|
|
$dbh->do("UPDATE paiduser SET paiduntil=DATE_SUB(paiduntil, INTERVAL ? MONTH) ".
|
|
"WHERE userid=?", undef, $months, $userid)
|
|
unless $is_perm;
|
|
|
|
# remove them from being a paid user if their time has run out
|
|
LJ::Pay::remove_paid_account($userid, undef, $is_perm)
|
|
unless $dbh->selectrow_array("SELECT paiduntil > NOW() FROM paiduser WHERE userid=?", undef, $userid);
|
|
|
|
# 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 $extra = $it ? " payment: $it->{'payid'}\[$it->{'piid'}]" : "";
|
|
LJ::statushistory_add($userid, undef, "revoke",
|
|
"item=paidacct; $months months; 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
|
|
|
|
unless (($account{$custom{'months'}}->{'amount'} == $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 = LWP::UserAgent->new(timeout => 6,
|
|
agent => "LJ-PayPalAuth/0.1");
|
|
|
|
# Create a request
|
|
my @urls = ('https://www.paypal.com/cgi-bin/webscr?cmd=_notify-validate',
|
|
'http://www.bradfitz.com/cgi-bin/paypalproxy.cgi');
|
|
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_cart {
|
|
my $cart = shift;
|
|
return undef unless $cart =~ /^(\d+)-(\d+)$/;
|
|
my ($payid, $anum) = ($1, $2);
|
|
my $dbh = LJ::get_db_writer();
|
|
my $cartobj = $dbh->selectrow_hashref("SELECT * FROM payments ".
|
|
"WHERE payid=$payid AND anum=$anum ".
|
|
"AND forwhat='cart'");
|
|
return undef unless $cartobj;
|
|
$cartobj->{'items'} = [];
|
|
my $sth = $dbh->prepare("SELECT * FROM payitems WHERE payid=$payid ORDER BY piid");
|
|
$sth->execute;
|
|
while (my $pi = $sth->fetchrow_hashref) {
|
|
push @{$cartobj->{'items'}}, $pi;
|
|
}
|
|
return $cartobj;
|
|
}
|
|
|
|
sub LJ::Pay::new_cart {
|
|
my $remote = shift;
|
|
my $dbh = LJ::get_db_writer();
|
|
my $anum = int(rand()*65536);
|
|
my $userid = $remote ? $remote->{'userid'} : 0;
|
|
$dbh->do("INSERT INTO payments (forwhat, anum, userid, datesent, used, mailed) ".
|
|
"VALUES ('cart', $anum, $userid, NOW(), 'C', 'C')");
|
|
my $payid = $dbh->{'mysql_insertid'};
|
|
return undef unless $payid;
|
|
LJ::Pay::payvar_append($payid, "creator_ip", LJ::get_remote_ip());
|
|
return LJ::Pay::load_cart("$payid-$anum");
|
|
}
|
|
|
|
sub LJ::Pay::payvar_add {
|
|
my ($payid, $k, $v) = @_;
|
|
my $dbh = LJ::get_db_writer();
|
|
LJ::Pay::payvar_append($payid, $k, $v)
|
|
unless $dbh->selectrow_array("SELECT payid FROM payvars ".
|
|
"WHERE payid=? AND pkey=? AND pval=?",
|
|
undef, $payid, $k, $v);
|
|
}
|
|
|
|
sub LJ::Pay::payvar_append {
|
|
my ($payid, $k, $v) = @_;
|
|
my $dbh = LJ::get_db_writer();
|
|
$dbh->do("INSERT INTO payvars (payid, pkey, pval) VALUES (?,?,?)",
|
|
undef, $payid, $k, $v);
|
|
}
|
|
|
|
sub LJ::Pay::payvar_set {
|
|
my ($payid, $k, $v) = @_;
|
|
my $dbh = LJ::get_db_writer();
|
|
$dbh->do("DELETE FROM payvars WHERE payid=? AND pkey=?", undef,
|
|
$payid, $k);
|
|
LJ::Pay::payvar_append($payid, $k, $v)
|
|
}
|
|
|
|
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 $item = shift;
|
|
return LJ::error("no cart") unless $cartobj;
|
|
my $dbh = LJ::get_db_writer();
|
|
$dbh->do("INSERT INTO payitems (payid, status, item, subitem, qty, rcptid, amt, rcptemail, anon, giveafter, token, tokenid) ".
|
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", undef,
|
|
$cartobj->{'payid'}, "cart",
|
|
map { $item->{$_} } qw(item subitem qty rcptid amt rcptemail anon giveafter token tokenid));
|
|
return LJ::error($dbh) if $dbh->err;
|
|
|
|
my $piid = $dbh->{'mysql_insertid'};
|
|
return LJ::error("Couldn't get piid") unless $piid;
|
|
$item->{'piid'} = $piid;
|
|
push @{$cartobj->{'items'}}, $item;
|
|
LJ::Pay::update_cart_total($cartobj);
|
|
|
|
return $item;
|
|
}
|
|
|
|
sub LJ::Pay::remove_cart_items {
|
|
my $cartobj = shift;
|
|
my @items = @_;
|
|
return 0 unless $cartobj;
|
|
return 1 unless @items;
|
|
|
|
my @ids = map { (ref $_ eq "HASH" ? $_->{'piid'} : $_) + 0 } @items;
|
|
my $in = join(',', @ids);
|
|
my @cp_ids = map { $_->{tokenid}+0 } grep { ref $_ eq "HASH" && $_->{item} eq 'coupon' } @items;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
# when removing a coupon from the cart, set the payid column back to NULL
|
|
if (@cp_ids) {
|
|
my $cp_in = join(',', @cp_ids);
|
|
$dbh->do("UPDATE coupon SET payid=0 WHERE cpid IN ($cp_in)");
|
|
}
|
|
|
|
$dbh->do("DELETE FROM payitems WHERE piid IN ($in) AND payid=?",
|
|
undef, $cartobj->{'payid'});
|
|
|
|
# remove items from the cartobj
|
|
@{$cartobj->{'items'}} = grep { my $id = $_->{'piid'}; ! grep { $id == $_ } @ids; } @{$cartobj->{'items'}};
|
|
|
|
LJ::Pay::update_cart_total($cartobj);
|
|
return 1;
|
|
}
|
|
|
|
sub LJ::Pay::update_shipping_cost {
|
|
my ($cartobj, $country) = @_;
|
|
return 0 unless $cartobj;
|
|
|
|
# should only have one shipping cost line
|
|
my @shipi = grep { $_->{'item'} eq "shipping" } @{$cartobj->{'items'}};
|
|
my $shipi;
|
|
|
|
# figure out shipping cost ($5, $3, $5, $3....)
|
|
my $ship_cost = 0;
|
|
my $last = 0;
|
|
foreach (grep { $_->{'item'} eq "clothes" } @{$cartobj->{'items'}}) {
|
|
my $this_cost = 5;
|
|
$this_cost = 3 if $last == 5;
|
|
$ship_cost += $this_cost;
|
|
$last = $this_cost;
|
|
}
|
|
|
|
# shipping on clothing only if outside US/Canada/Territories and order amt is non-zero
|
|
if ((grep { $_->{'item'} eq "clothes" } @{$cartobj->{'items'}}) &&
|
|
$country ne "US" && # United States
|
|
$country ne "CA" && # Canada
|
|
$country ne "PR" && # Puerto Rico
|
|
$country ne "GU") # Guam
|
|
{
|
|
# get the first one, or make one
|
|
$shipi = shift @shipi;
|
|
unless ($shipi) {
|
|
$shipi = {
|
|
'item' => 'shipping',
|
|
'rcptid' => 0,
|
|
'amt' => $ship_cost,
|
|
};
|
|
die "Couldn't add shipping cost" unless
|
|
LJ::Pay::add_cart_item($cartobj, $shipi);
|
|
}
|
|
}
|
|
|
|
# delete extra shipping items
|
|
LJ::Pay::remove_cart_items($cartobj, @shipi) if @shipi;
|
|
|
|
# update shipping cost, if they're subject to shipping
|
|
if ($shipi && $shipi->{'amt'} != $ship_cost) {
|
|
$shipi->{'amt'} = $ship_cost;
|
|
my $dbh = LJ::get_db_writer();
|
|
$dbh->do("UPDATE payitem SET amt=? WHERE piid=?", undef,
|
|
$ship_cost, $shipi->{'piid'});
|
|
LJ::Pay::update_cart_total($cartobj);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub LJ::Pay::is_tangible {
|
|
my $it = shift;
|
|
return ($it->{'item'} eq 'clothes' || $it->{'item'} eq 'coupon');
|
|
}
|
|
|
|
|
|
# takes in a cart object and returns the following hash keys/values:
|
|
#
|
|
# adj_amt_tot: total adjusted price of cart after coupons are applied
|
|
# adj_amt_int: total adjusted price of intangible items in cart after coupons are applied
|
|
# adj_amt_tan: total adjusted price of tangible items in cart after coupons are applied
|
|
# adj_amt_cp: total adjusted price of coupons being _purchased_ after coupons are applied
|
|
#
|
|
# cart_amt_tot: total unadjusted price of cart before coupons are applied
|
|
# cart_amt_int: total unadjusted price of intangible items in cart before coupons are applied
|
|
# cart_amt_tan: total unadjusted price of tangible items in cart before coupons are applied
|
|
# cart_amt_cp: total unadjusted price of coupons being _purchased_ before coupons are applied
|
|
#
|
|
# cp_amt_tot: total dollar amount of coupons in the cart (all types)
|
|
# cp_amt_gen: total dollar amount of general (universal) coupons in cart
|
|
# cp_amt_int: total dollar amount of intangible-only coupons in cart
|
|
# cp_amt_tan: total dollar amount of tangible-only coupons in cart
|
|
#
|
|
# cp_used_tot: total dollar amount of used coupons of all types
|
|
# cp_used_gen: total dollar amount of general (universal) coupons applied to cart
|
|
# cp_used_int: total dollar amount of intangible-only coupons applied to cart
|
|
# cp_used_tan: total dollar amount of tangible-only coupons applied to cart
|
|
#
|
|
# cp_unused_tot: total dollar amount of unused coupons of all types
|
|
# cp_unused_gen: total dollar amount of general (universal) coupons unused
|
|
# cp_unused_int: total dollar amount of unused intangible-only coupons in cart
|
|
# cp_unused_tan: total dollar amount of unused tangible-only coupons in cart
|
|
#
|
|
sub LJ::Pay::coupon_reduce {
|
|
my $cartobj = shift;
|
|
|
|
# types for the following hashes:
|
|
# - tot: total amount for all item types
|
|
# - gen: general (universal) amount
|
|
# - tan: tangible-only amount
|
|
# - int: intangible-only amount
|
|
|
|
my %cp_amt = (); # type => total coupon amount of this type
|
|
my %cp_unused = (); # type => total coupon amount unused of this type
|
|
my %cp_used = (); # type => total coupon amount used of this type
|
|
|
|
# types for the following hashes:
|
|
# - tot: total amount of all item types
|
|
# - tan: tangible item amount
|
|
# - int: intangible item amount
|
|
# - cp: coupons the user is buying
|
|
|
|
my %cart_amt = (); # type => total amount in cart of this type
|
|
my %adj_amt = (); # type => adjusted cart amount of this type after coupons applied
|
|
|
|
foreach my $it (@{$cartobj->{'items'}}) {
|
|
|
|
# item being purchased
|
|
if ($it->{amt} > 0) {
|
|
|
|
my $type = LJ::Pay::is_tangible($it) ? 'tan' : 'int';
|
|
|
|
# NOTE: if the user is buying a coupon with a positive dollar amount, then it is a
|
|
# general coupon that can be used to buy anything. we can't apply any coupons to
|
|
# other coupon prices or else we open up a hole by which users can transcent their
|
|
# tangible/intangible limitations for free. bleh.
|
|
$type = 'cp' if $it->{item} eq 'coupon';
|
|
|
|
# add to purchase total for this type
|
|
$cart_amt{$type} += $it->{amt};
|
|
|
|
# also update total cart amount
|
|
$cart_amt{tot} += $it->{amt};
|
|
|
|
# applying a coupon given to them
|
|
} elsif ($it->{item} eq "coupon") {
|
|
|
|
# keep a tally of total amount of coupons being used
|
|
# on this order (NOT purchased via this order)
|
|
if ($it->{subitem} =~ /^dollaroff(tan|int)?/) {
|
|
$cp_amt{$1 || 'gen'} += abs($it->{amt});
|
|
$cp_amt{tot} += abs($it->{amt});
|
|
|
|
# otherwise could be a 'freeclothingitem' coupon type
|
|
# -- just treat this as a tangible coupon and it will be
|
|
# handled properly
|
|
} elsif ($it->{subitem} =~ /^freeclothingitem/) {
|
|
$cp_amt{tan} += abs($it->{amt});
|
|
}
|
|
}
|
|
}
|
|
|
|
# coupons being _purchased_ don't get adjusted, but for the sake of uniformity,
|
|
# we'll go ahead and add a key for their 'adjusted price' and set it the same as
|
|
# their total
|
|
$adj_amt{cp} = $cart_amt{cp};
|
|
|
|
# 1) apply coupons applying to items of each type
|
|
# - how much coupon was unused?
|
|
# - how much still needs to be paid for?
|
|
foreach my $type (qw(tan int)) {
|
|
|
|
# assume we'll use all of it and correct later
|
|
$adj_amt{$type} = $cart_amt{$type} - $cp_amt{$type};
|
|
$cp_unused{$type} = 0;
|
|
|
|
# if less than 0, we used too much, so decide how much was unused
|
|
if ($adj_amt{$type} < 0) {
|
|
$cp_unused{$type} = abs($adj_amt{$type}); # unused coupons of this type
|
|
$adj_amt{$type} = 0; # adjusted amount is 0
|
|
}
|
|
}
|
|
|
|
# 2) if we have general purpose coupons and there are balances
|
|
# left, apply those to the adjusted amounts now
|
|
$cp_unused{gen} = $cp_amt{gen}; # haven't used any general coupons yet
|
|
foreach my $type (qw(tan int)) {
|
|
|
|
# assume we'll use all of it and correct later
|
|
$adj_amt{$type} -= $cp_unused{gen};
|
|
$cp_unused{gen} = 0;
|
|
|
|
# if less than 0, we used too much, so decide how much was unused
|
|
if ($adj_amt{$type} < 0) {
|
|
$cp_unused{gen} = abs($adj_amt{$type}); # unused general coupons
|
|
$adj_amt{$type} = 0; # adjusted amount is 0
|
|
}
|
|
|
|
}
|
|
|
|
# fill in how much was used
|
|
%cp_used = map { $_ => $cp_amt{$_} - $cp_unused{$_} } qw(gen tan int);
|
|
|
|
# total (gen) adjusted amount is total cart amount - sum(all coupons used)
|
|
$adj_amt{tot} = $cart_amt{tot};
|
|
$adj_amt{tot} -= $cp_used{$_} foreach qw(gen tan int);
|
|
|
|
# total coupon usage
|
|
$cp_used{tot} = $cart_amt{tot} - $adj_amt{tot};
|
|
$cp_unused{tot} = $cp_amt{tot} - $cp_used{tot};
|
|
|
|
return {
|
|
|
|
# adjusted amounts
|
|
(map { ("adj_amt_$_" => $adj_amt{$_}+0) } qw(tot cp tan int)),
|
|
|
|
# cart totals for different cateogires
|
|
(map { ("cart_amt_$_" => $cart_amt{$_}+0) } qw(tot cp tan int)),
|
|
|
|
# total acoupon amounts in cart
|
|
(map { ("cp_amt_$_" => $cp_amt{$_}+0) } qw(tot gen tan int)),
|
|
|
|
# coupon amounts used
|
|
(map { ("cp_used_$_" => $cp_used{$_}+0) } qw(tot gen tan int)),
|
|
|
|
# coupon amounts unused
|
|
(map { ("cp_unused_$_" => $cp_unused{$_}+0) } qw(tot gen tan int)),
|
|
};
|
|
}
|
|
|
|
sub LJ::Pay::send_coupon_email {
|
|
my ($u, $token, $amt, $type) = @_;
|
|
return undef unless $u && $token && defined $amt;
|
|
my $email = ref $u ? $u->{'email'} : $u;
|
|
return undef unless $email;
|
|
|
|
my $inttxt;
|
|
if ($type eq 'int') {
|
|
$inttxt .= "This coupon is only valid for intangible items such as paid accounts ";
|
|
$inttxt .= "and bonus features. It cannot be used to buy other coupons or to ";
|
|
$inttxt .= "buy clothing.\n\n";
|
|
} elsif ($type eq 'tan') {
|
|
$inttxt .= "This coupon is only valid for tangible items such as tee shirts and ";
|
|
$inttxt .= "hoodies. It cannot be used to buy other coupons or intangible items ";
|
|
$inttxt .= "such as paid accounts.\n\n";
|
|
}
|
|
|
|
my $storetxt;
|
|
unless ($type eq 'int') {
|
|
$storetxt .= "$LJ::SITENAMESHORT store:\n";
|
|
$storetxt .= " - $LJ::SITEROOT/store/\n\n";
|
|
}
|
|
|
|
# print dollars
|
|
my $damt = sub { sprintf("\$%.02f", shift()) };
|
|
|
|
return LJ::send_mail({
|
|
'to' => $email,
|
|
'from' => $LJ::ACCOUNTS_EMAIL,
|
|
'fromname' => $LJ::SITENAMESHORT,
|
|
'wrap' => 1,
|
|
'charset' => 'utf-8',
|
|
'subject' => "Coupon",
|
|
'body' =>
|
|
"$LJ::SITENAMESHORT coupon 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 " .
|
|
"merchandise and/or services:\n\n".
|
|
|
|
"$LJ::SITENAMESHORT services:\n" .
|
|
" - $LJ::SITEROOT/pay/\n\n" .
|
|
|
|
$storetxt .
|
|
|
|
"NOTE: Your coupon 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" .
|
|
|
|
"$LJ::SITENAMESHORT Team",
|
|
});
|
|
}
|
|
|
|
# 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;
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
# clothing piids which have been coupon'ed already
|
|
my %free_clothes;
|
|
|
|
foreach my $it (@{$cartobj->{'items'}}) {
|
|
next unless $it->{'item'} eq "coupon";
|
|
my ($type, $arg) = split(/-/, $it->{'subitem'});
|
|
next unless $type eq "freeclothingitem";
|
|
|
|
my $amt = 0;
|
|
|
|
# find most expensive item of clothing that
|
|
# hasn't been given free already
|
|
my ($max, $maxid);
|
|
foreach my $clit (@{$cartobj->{'items'}}) {
|
|
next unless $clit->{'item'} eq "clothes";
|
|
next if $free_clothes{$clit->{'piid'}};
|
|
next if $clit->{'amt'} < $max;
|
|
|
|
# check to see if this product type is flagged as not
|
|
# being valid for free clothing items
|
|
my $cltype = (split("-", $clit->{'subitem'}))[0];
|
|
next if $LJ::Pay::product{"clothes-$cltype"}->[2];
|
|
|
|
$max = $clit->{'amt'};
|
|
$maxid = $clit->{'piid'};
|
|
}
|
|
if ($max) {
|
|
$amt = $max;
|
|
$free_clothes{$maxid} = 1;
|
|
}
|
|
|
|
# remove zero dollar coupons from cart
|
|
if ($amt == 0) {
|
|
$dbh->do("DELETE FROM payitems WHERE piid=?", undef, $it->{'piid'});
|
|
next;
|
|
}
|
|
|
|
if ($amt != $it->{'amt'}) {
|
|
$it->{'amt'} = -$amt;
|
|
$dbh->do("UPDATE payitems SET amt=? WHERE piid=?", undef, $it->{'amt'}, $it->{'piid'});
|
|
}
|
|
}
|
|
|
|
# analyze cart items to find total amounts
|
|
my $amts = LJ::Pay::coupon_reduce($cartobj);
|
|
|
|
# update payments with adjusted price of cart after coupons are applied
|
|
$dbh->do("UPDATE payments SET amount=? WHERE payid=? AND mailed='C' AND forwhat='cart'",
|
|
undef, $amts->{'adj_amt_tot'}, $cartobj->{'payid'});
|
|
return 1;
|
|
}
|
|
|
|
sub LJ::Pay::can_mod_cart {
|
|
my $cartobj = shift;
|
|
return 0 unless $cartobj;
|
|
return 0 if $cartobj->{method};
|
|
return 1;
|
|
}
|
|
|
|
sub LJ::Pay::can_checkout_cart {
|
|
my $cartobj = shift;
|
|
return 0 unless $cartobj;
|
|
return 0 unless $cartobj->{mailed} eq 'C';
|
|
return 0 unless @{$cartobj->{items}};
|
|
return 1;
|
|
}
|
|
|
|
sub LJ::Pay::cart_contains_coppa {
|
|
my $cartobj = shift;
|
|
return 0 unless $cartobj && @{$cartobj->{items}};
|
|
return scalar grep { $_->{item} eq 'coppa' } @{$cartobj->{items}};
|
|
}
|
|
|
|
sub LJ::Pay::cart_needs_shipping {
|
|
my $cartobj = shift;
|
|
return 0 unless $cartobj && @{$cartobj->{'items'}};
|
|
return scalar grep { LJ::Pay::item_needs_shipping($_) } @{$cartobj->{'items'}};
|
|
}
|
|
|
|
sub LJ::Pay::item_needs_shipping {
|
|
return ($_[0]->{'item'} eq "clothes");
|
|
}
|
|
|
|
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 eq 'HASH') {
|
|
# allow 'subitem' or size key so that we can pass either
|
|
# a row from payitems or a row from paidexp with the "short" flag
|
|
$subitem = $item->{'subitem'} || $item->{'size'};
|
|
$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::Pay::size{$size}->[1],
|
|
$LJ::Pay::color{$color},
|
|
$LJ::Pay::product{"clothes-$type"}->[0]);
|
|
}
|
|
|
|
if ($item eq "paidacct") {
|
|
return "Paid Account" . ($short ? "" : " - $LJ::Pay::account{$qty}->{'name'}");
|
|
}
|
|
|
|
if ($item eq "perm") {
|
|
return "Permanent Account";
|
|
}
|
|
|
|
if ($item eq "rename") {
|
|
return "Rename Token";
|
|
}
|
|
|
|
if ($item eq "coppa") {
|
|
return "Age Verification (for COPPA)";
|
|
}
|
|
|
|
if ($item eq "coupon") {
|
|
my ($type) = split(/-/, $subitem);
|
|
if ($type eq "freeclothingitem") {
|
|
return "Free clothing item";
|
|
}
|
|
if ($type =~ /^dollaroff(int|tan)?/) {
|
|
return "Coupon" . ($1 ? ($1 eq 'tan' ? ", tangible" : ", intangible") : "");
|
|
}
|
|
}
|
|
|
|
if (LJ::Pay::is_bonus($item, 'bool')) {
|
|
my $bitem = $LJ::Pay::bonus{$item};
|
|
return $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) . " " . $bitem->{'name'} .
|
|
($short ? "" : (" - " . ($sizeit->{'qty'}->{$qty}->{'name'} || $qty)));
|
|
}
|
|
|
|
return "$item-$subitem";
|
|
}
|
|
|
|
# there was a race condition when 'pay_updateaccounts' and 'expiring' ran at the same time,
|
|
# so now we get a lock and re-verify our data afterwards; closures to make things simple
|
|
sub LJ::Pay::get_lock {
|
|
my $u = shift;
|
|
my $userid = LJ::want_userid($u);
|
|
return undef unless $userid;
|
|
|
|
my $key = "acctupdate:$userid";
|
|
my $dbh = LJ::get_db_writer();
|
|
return LJ::get_lock($dbh, "global", $key);
|
|
};
|
|
|
|
sub LJ::Pay::release_lock {
|
|
my $u = shift;
|
|
my $userid = LJ::want_userid($u);
|
|
return undef unless $userid;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
my $key = "acctupdate:$userid";
|
|
return LJ::release_lock($dbh, "global", $key);
|
|
};
|
|
|
|
# is the given item a paidaccount add-on?
|
|
# thereby requiring checks for paid account existence?
|
|
sub LJ::Pay::is_bonus {
|
|
my ($it, $type) = @_;
|
|
my $item = ref $it eq 'HASH' ? $it->{'item'} : $it;
|
|
return undef unless defined $LJ::Pay::bonus{$item};
|
|
return undef if $type && $LJ::Pay::bonus{$item}->{'type'} ne $type;
|
|
return 1;
|
|
};
|
|
|
|
# given a cart can we add a given item?
|
|
sub LJ::Pay::can_apply_sized_bonus {
|
|
my ($u, $cartobj, $item, $size, $qty) = @_;
|
|
my $userid = LJ::want_userid($u);
|
|
|
|
# easy/obvious checks
|
|
return undef unless $userid && LJ::Pay::is_bonus($item, 'sized');
|
|
|
|
# if the caller doesn't specify a qty they are trying to add, just
|
|
# validate items already in the cart
|
|
$qty ||= 0;
|
|
|
|
# is there immediately applying paid time in the cart?
|
|
my $cart_paid_immed = undef;
|
|
|
|
# now go through the current cart and see what they have already
|
|
if ($cartobj) {
|
|
|
|
# will be used for checks later on "dimension signature"
|
|
my ($prev_exp, $prev_size) = LJ::Pay::get_bonus_dim($userid, $item);
|
|
|
|
foreach my $it (@{$cartobj->{'items'}}) {
|
|
next unless $it->{'rcptid'} == $userid;
|
|
|
|
# collect information on when paid account starts in this cart
|
|
$cart_paid_immed = 1
|
|
if $it->{'item'} eq 'paidacct' && ! $it->{'giveafter'};
|
|
|
|
next unless $it->{'item'} eq $item;
|
|
|
|
# can't have a giveafter date on sized items
|
|
return undef if $it->{'giveafter'};
|
|
|
|
# subitem field contains a few useful bits of info
|
|
my ($itsize, $curr_exp, $curr_size) = split("-", $it->{'subitem'});
|
|
|
|
# if no size specified, then just verify that all sizes in the cart are equal
|
|
$size ||= $itsize;
|
|
|
|
# can buy multiple items, but only of the same size
|
|
return undef if $itsize != $size;
|
|
|
|
# when applying sized bonus, we have to make sure that no other sized bonus features have been
|
|
# applied since this one was added to the cart, so check the previous "dimension signature"
|
|
# to decide if this item can be legally applied
|
|
return undef unless $prev_exp == $curr_exp && $prev_size == $curr_size;
|
|
|
|
# this is an extension to something already in the cart
|
|
$qty += $it->{'qty'};
|
|
|
|
# can't have more than 12 months in cart
|
|
return undef if $qty > 12;
|
|
}
|
|
}
|
|
|
|
# now time to run some checks on the database
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
# if no paid account in cart starting immediately, check in the database
|
|
# to see if there is currently paid time there
|
|
unless ($cart_paid_immed) {
|
|
|
|
# sometimes users have the paid cap with no paiduser row in the database, eg when
|
|
# they have a permanent account ... assume if they have a perm account then
|
|
# they are paid forever
|
|
unless ($dbh->selectrow_array("SELECT COUNT(*) FROM paiduser WHERE userid=? AND paiduntil>NOW()", undef, $userid)) {
|
|
|
|
# if the query above failed, check to see if they have the paid cap
|
|
$u = LJ::want_user($u);
|
|
return undef unless $u && $u->{'caps'} & (1 << $LJ::Pay::capinf{'perm'}->{'bit'});
|
|
}
|
|
}
|
|
|
|
# now let's see what's in the database, with regards to this bonus item
|
|
my $row = $dbh->selectrow_hashref("SELECT *, " .
|
|
"(NOW() + INTERVAL ? MONTH <= expdate) AS 'is_short', " .
|
|
"(IF(size=?, expdate, NOW()) + INTERVAL ? MONTH > NOW() + INTERVAL 12 MONTH) AS 'is_long' " .
|
|
"FROM paidexp WHERE userid=? AND item=?", undef, $qty, $size, $qty, $userid, $item);
|
|
|
|
# if nothing in database, the checks we've already done are sufficient
|
|
return 1 unless $row;
|
|
|
|
# now we know there was a $row in the db
|
|
|
|
# can't apply if they already have stored, therefore presumably no paid account?
|
|
return undef if $row->{'daysleft'};
|
|
|
|
# can't apply if expiration date won't extend past that of their current time
|
|
# unless the size is the same as their current size
|
|
return undef if $row->{'is_short'} && $row->{'size'} != $size;
|
|
|
|
# can't apply more than one year in the future
|
|
return undef if $row->{'is_long'};
|
|
|
|
# can't downgrade to a lower size
|
|
return undef if $size < $row->{'size'};
|
|
|
|
return 1;
|
|
};
|
|
|
|
sub LJ::Pay::can_apply_bool_bonus {
|
|
my ($u, $cartobj, $item) = @_;
|
|
my $userid = LJ::want_userid($u);
|
|
|
|
# easy/obvious checks
|
|
return undef unless $userid && LJ::Pay::is_bonus($item, 'bool');
|
|
|
|
# when does paid time in the cart begin?
|
|
my $cart_paid_start = undef;
|
|
my $cart_bonus_start = undef;
|
|
|
|
if ($cartobj) {
|
|
|
|
# check the cart to see if we can immediately exonerate this bonus feature
|
|
# without even looking in the database.
|
|
foreach my $it (@{$cartobj->{'items'}}) {
|
|
next unless $it->{'rcptid'} == $userid;
|
|
|
|
# can't buy bool bonus features for permanent accounts
|
|
return undef if $it->{'item'} eq 'perm';
|
|
|
|
# calculate starting time of first applying amount of paid time in the cart
|
|
if ($it->{'item'} eq 'paidacct') {
|
|
|
|
if ($it->{'giveafter'}) {
|
|
$cart_paid_start = $it->{'giveafter'} if ! defined $cart_paid_start || $it->{'giveafter'} < $cart_paid_start;
|
|
next;
|
|
}
|
|
|
|
# no giveafter time, applies immediately
|
|
$cart_paid_start = 0;
|
|
next;
|
|
}
|
|
|
|
# calculate starting time of this bonus item
|
|
if ($it->{'item'} eq $item) {
|
|
|
|
if ($it->{'giveafter'}) {
|
|
$cart_bonus_start = $it->{'giveafter'} if ! defined $cart_bonus_start || $it->{'giveafter'} < $cart_bonus_start;
|
|
next;
|
|
}
|
|
|
|
# no giveafter time, applies immediately
|
|
$cart_bonus_start = 0;
|
|
next;
|
|
}
|
|
}
|
|
|
|
# immediately applying paid account == we're in the clear
|
|
# - note that undef == 0 returns true since undef gets converted to numeric
|
|
# context (0) before the comparison is done, blah perl
|
|
return 1 if defined $cart_paid_start && $cart_paid_start == 0;
|
|
return 1 if defined $cart_bonus_start && defined $cart_paid_start &&
|
|
$cart_bonus_start >= $cart_paid_start;
|
|
}
|
|
|
|
# is the specified userid a permanent account? if so there's a problem
|
|
$u = LJ::load_userid($userid, "force");
|
|
return undef if ! $u || $u->{'caps'} & (1 << $LJ::Pay::capinf{'perm'}->{'bit'});
|
|
|
|
# can be applied if they have a currently unexpired paid account
|
|
my $dbh = LJ::get_db_writer();
|
|
my $paiduntil = $dbh->selectrow_array("SELECT UNIX_TIMESTAMP(paiduntil) FROM paiduser WHERE userid=? AND paiduntil>NOW()",
|
|
undef, $userid);
|
|
|
|
# at this point we know that paid time in the cart doesn't immediately
|
|
# exonerate the bonus feature, because we would have returned already
|
|
return undef if ! $paiduntil || $paiduntil < $cart_bonus_start;
|
|
|
|
# everything checked out
|
|
return 1;
|
|
}
|
|
|
|
# get dimensions of current sized bonus block
|
|
sub LJ::Pay::get_bonus_dim {
|
|
my ($u, $itemname) = @_;
|
|
my $userid = LJ::want_userid($u);
|
|
return undef unless $userid;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
my ($exptime, $size) = $dbh->selectrow_array("SELECT UNIX_TIMESTAMP(expdate), size " .
|
|
"FROM paidexp WHERE userid=? AND item=?",
|
|
undef, $userid, $itemname);
|
|
return ($exptime || 0, $size || 0);
|
|
}
|
|
|
|
# upgrade or extend the length of a bonus item
|
|
sub LJ::Pay::apply_bonus_item {
|
|
my ($u, $item, $subitem, $qty, $payid) = @_;
|
|
|
|
# allow u/payitem objects passed optionally
|
|
my $userid = LJ::want_userid($u);
|
|
if (ref $item) {
|
|
$subitem = $item->{'subitem'};
|
|
$qty = $item->{'qty'};
|
|
$payid = $item->{'payid'};
|
|
$item = $item->{'item'};
|
|
}
|
|
|
|
# userid and item are required regardless of bonus feature type
|
|
return undef unless $userid && $item;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
# does an existing paidexp row exist?
|
|
my $exp = $dbh->selectrow_hashref("SELECT userid, item, size, expdate, daysleft, " .
|
|
"UNIX_TIMESTAMP(expdate) AS 'exptime', " .
|
|
"(expdate > NOW()) AS 'unexpired' " .
|
|
"FROM paidexp WHERE userid=? AND item=?",
|
|
undef, $userid, $item);
|
|
|
|
# if no row in database, fill in $exp with good values
|
|
$exp ||= {
|
|
'userid' => $userid,
|
|
'item' => $item,
|
|
'size' => 0,
|
|
'expdate' => undef,
|
|
'daysleft' => 0,
|
|
'exptime' => 0,
|
|
'unexpired' => 0,
|
|
};
|
|
|
|
my $new_size = $exp->{'size'};
|
|
|
|
# activate cap if necessary
|
|
if (my $cap = $LJ::Pay::bonus{$item}->{'cap'}) {
|
|
LJ::modify_caps($userid, [$cap], [])
|
|
or return undef;
|
|
}
|
|
|
|
# actions for 'sized' bonus feature type
|
|
# -- need to check that account is still in size/exp state it was in
|
|
# when this item was purchased so people can't be comp'd more than
|
|
# once for existing paid time on upgrades
|
|
|
|
my $sized_upgrade = 0;
|
|
|
|
if (LJ::Pay::is_bonus($item, 'sized')) {
|
|
return undef unless $subitem;
|
|
|
|
# make sure exp/size signature in subitem still matches
|
|
my ($it_size, $old_exptime, $old_size) = split("-", $subitem);
|
|
|
|
# if a payid is passed, then first check to make sure that no other payitems
|
|
# in this cart have been applied, altering the exp/size signature and making
|
|
# this check return false-positives
|
|
|
|
unless ($payid &&
|
|
$dbh->selectrow_array("SELECT COUNT(*) FROM payitems " .
|
|
"WHERE payid=? AND item=? AND subitem=? AND status='done'",
|
|
undef, $payid, $item, $subitem))
|
|
{
|
|
|
|
# zero-fill
|
|
$old_exptime ||= 0;
|
|
$old_size ||= 0;
|
|
|
|
# check for exptime/size mismatch, now that we know it's necessary
|
|
unless ($old_exptime == $exp->{'exptime'} && $old_size == $exp->{'size'}) {
|
|
|
|
# all bonus items of this type have the same size and exptime/oldsize signature
|
|
# by the rules applied to them when they entered the cart.
|
|
#
|
|
# so if one item fails, we go ahead and mark them all as having failed.
|
|
# the caller (pay.pl) will have to be smart enough to know to not try to process
|
|
# subsequent items of the failed bonus type
|
|
|
|
LJ::statushistory_add($userid, undef, 'pay_modify',
|
|
"ERROR: cannot apply bonus feature: $item, " .
|
|
"${old_exptime}x${old_size} != $exp->{'exptime'}x$exp->{'size'}");
|
|
|
|
$dbh->do("UPDATE payitems SET status='done' WHERE payid=? AND item=? AND subitem=?",
|
|
undef, $payid, $item, $subitem);
|
|
|
|
return undef;
|
|
}
|
|
}
|
|
|
|
# could either be upgrade or extension, but either way we adopt the item's size
|
|
$new_size = $it_size;
|
|
|
|
# means that we are upgrading a sized bonus item, so the time added should start
|
|
# from now, not the current expdate
|
|
$sized_upgrade = 1 unless $old_size == $new_size;
|
|
}
|
|
|
|
# insert a new row, or add time to old one
|
|
# - this code somewhat duplicates functionality from LJ::Pay::activate_frozen_bonus
|
|
# but we need to extend the expdate by $qty months anyway, so we'll just do the
|
|
# daysleft activation here as well, avoiding some queries
|
|
{
|
|
# expdate calculation is tricky
|
|
# [(expdate || NOW()) + INTERVAL $qty MONTH] + INTERVAL $daysleft DAY
|
|
|
|
# expiration extends off current expdate if there's a currently unexpired item of
|
|
# the same size. otherwise it's an upgrade and starts from NOW()
|
|
my $expdate = $exp->{'unexpired'} && ! $sized_upgrade
|
|
? $dbh->quote($exp->{'expdate'}) : "NOW()";
|
|
|
|
if ($qty) {
|
|
my $qqty = $dbh->quote($qty || 0);
|
|
$expdate = "($expdate + INTERVAL $qqty MONTH)";
|
|
}
|
|
if ($exp->{'daysleft'}) {
|
|
my $qdaysleft = $dbh->quote($exp->{'daysleft'});
|
|
$expdate = "($expdate + INTERVAL $qdaysleft DAY)";
|
|
}
|
|
|
|
# update / insert paidexp row
|
|
$dbh->do("REPLACE INTO paidexp (userid, item, size, expdate, daysleft) " .
|
|
"VALUES (?, ?, ?, $expdate, 0)", undef, $userid, $item, $new_size);
|
|
return undef if $dbh->err;
|
|
}
|
|
|
|
# call any application hooks for this bonus feature
|
|
my $apply_hook = $LJ::Pay::bonus{$item}->{'apply_hook'};
|
|
if ($apply_hook && ref $apply_hook eq 'CODE') {
|
|
# apply_hook needs a real $u object
|
|
$u = ref $u ? $u : LJ::load_userid($u);
|
|
$apply_hook->($u, $item);
|
|
}
|
|
|
|
# log this bonus feature activation
|
|
{
|
|
my $msg = "adding bonus feature: item=$item; ";
|
|
if (LJ::Pay::is_bonus($item, 'sized')) {
|
|
$msg .= "size=$exp->{'size'}";
|
|
$msg .= "=>$new_size" if $exp->{'size'} != $new_size;
|
|
$msg .= "; ";
|
|
}
|
|
$msg .= "old_expdate=$exp->{'expdate'}; applying $qty months, $exp->{'daysleft'} existing days";
|
|
|
|
LJ::statushistory_add($userid, undef, 'pay_modify', $msg);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub LJ::Pay::expire_bonus {
|
|
my ($u, $item) = @_;
|
|
|
|
# allow u/payitem objects passed optionally
|
|
my $userid = LJ::want_userid($u);
|
|
$item = $item->{'item'} if ref $item;
|
|
return undef unless $userid;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
# we can either operate on one given item or all items for a user
|
|
my $itemand = (" AND item=" . $dbh->quote($item)) if $item;
|
|
|
|
# hard-validate constraints on the paidexp table in here
|
|
# - this is probably done by the caller too, but outside of a lock
|
|
my $sth = $dbh->prepare("SELECT item FROM paidexp WHERE userid=?$itemand " .
|
|
"AND (daysleft=0 OR daysleft IS NULL) " .
|
|
"AND expdate < NOW() AND expdate > '0000-00-00'");
|
|
$sth->execute($userid);
|
|
my @activated = ();
|
|
while (my ($item) = $sth->fetchrow_array) {
|
|
next unless LJ::Pay::is_bonus($item);
|
|
|
|
# remove cap if there's one associated with this bonus item
|
|
if (my $cap = $LJ::Pay::bonus{$item}->{'cap'}) {
|
|
LJ::modify_caps($userid, [], [$cap])
|
|
or return undef;
|
|
}
|
|
|
|
# remove paidexp row
|
|
$dbh->do("DELETE FROM paidexp WHERE userid=? AND item=?", undef, $userid, $item);
|
|
|
|
# log this bonus feature expiration
|
|
LJ::statushistory_add($u, undef, 'pay_modify', "expiring bonus feature: $item");
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
# activates frozen bonus features
|
|
# - returns array of hashrefs: { item => { itemname, size, days_activated }
|
|
sub LJ::Pay::activate_frozen_bonus {
|
|
my ($u, $item) = @_; # item is optional
|
|
|
|
# allow u/payitem objects passed optionally
|
|
my $userid = LJ::want_userid($u);
|
|
$item = $item->{'item'} if ref $item;
|
|
return undef unless $userid;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
# we can either operate on one given item or all items for a user
|
|
my $itemand = (" AND item=" . $dbh->quote($item)) if $item;
|
|
|
|
# see if there is existing time
|
|
my $sth = $dbh->prepare("SELECT item, size, expdate, daysleft, (expdate > NOW()) AS 'unexpired' " .
|
|
"FROM paidexp WHERE daysleft>0 AND userid=?$itemand");
|
|
$sth->execute($userid);
|
|
my @activated = ();
|
|
while (my ($item, $size, $expdate, $daysleft, $unexpired) = $sth->fetchrow_array) {
|
|
next unless LJ::Pay::is_bonus($item);
|
|
|
|
# it would generally suffice to set expdate to NOW() + INTERVAL daysleft DAY, but to be more
|
|
# robust we want to handle the case where there are daysleft in the db, but the item isn't
|
|
# expired yet
|
|
my $base = $unexpired ? $dbh->quote($expdate) : "NOW()";
|
|
|
|
# update database if we found some (need select above to fetch daysleft)
|
|
$dbh->do("UPDATE paidexp SET expdate=($base + INTERVAL ? DAY), daysleft=0 " .
|
|
"WHERE userid=? AND item=?", undef, $daysleft, $userid, $item);
|
|
return undef if $dbh->err;
|
|
|
|
# reactivate caps if necessary
|
|
if (my $cap = $LJ::Pay::bonus{$item}->{'cap'}) {
|
|
LJ::modify_caps($userid, [$cap], [])
|
|
or return undef;
|
|
}
|
|
|
|
# log this bonus feature activation
|
|
LJ::statushistory_add($userid, undef, 'pay_modify',
|
|
"adding bonus feature: item=$item; old_expdate=$expdate; " .
|
|
"applying $daysleft existing days");
|
|
|
|
push @activated, { 'item' => $item, 'size' => $size, 'daysleft' => $daysleft };
|
|
}
|
|
|
|
return @activated;
|
|
}
|
|
|
|
# activates frozen bonus features
|
|
# - returns array of hashrefs: { item, size, daysleft frozen }
|
|
sub LJ::Pay::freeze_bonus {
|
|
my ($u, $item) = @_; # item is optional
|
|
|
|
# allow u/payitem objects passed optionally
|
|
my $userid = LJ::want_userid($u);
|
|
$item = $item->{'item'} if ref $item;
|
|
return undef unless $userid;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
# we can either operate on one given item or all items for a user
|
|
my $itemand = $item ? (" AND item=" . $dbh->quote($item)) : "";
|
|
|
|
# see if there is existing time
|
|
my $sth = $dbh->prepare("SELECT item, size, (TO_DAYS(expdate)-TO_DAYS(NOW())+daysleft) AS 'new_daysleft' " .
|
|
"FROM paidexp WHERE expdate>NOW() AND userid=?$itemand");
|
|
$sth->execute($userid);
|
|
my @deactivated = ();
|
|
while (my ($item, $size, $new_daysleft) = $sth->fetchrow_array) {
|
|
|
|
# this shouldn't ever get triggered
|
|
next unless LJ::Pay::is_bonus($item);
|
|
|
|
# remove cap (if necessary) and run applicable hooks
|
|
if (my $cap = $LJ::Pay::bonus{$item}->{'cap'}) {
|
|
LJ::modify_caps($userid, [], [$cap])
|
|
or return 0;
|
|
}
|
|
|
|
# set expdate to now and save current time in daysleft
|
|
# - to be robust, handle the case where there are currently daysleft but the expdate>NOW(),
|
|
# even though it technically shouldn't happen.
|
|
if ($new_daysleft) {
|
|
$dbh->do("UPDATE paidexp SET daysleft=?, expdate=NOW() " .
|
|
"WHERE userid=? AND item=?", undef, $new_daysleft, $userid, $item);
|
|
|
|
# if daysleft ended up being 0 above, delete the row
|
|
} else {
|
|
$dbh->do("DELETE FROM paidexp WHERE userid=? AND item=? AND (daysleft=0 OR daysleft IS NULL)",
|
|
undef, $userid, $item);
|
|
}
|
|
|
|
# log this bonus feature expiration
|
|
LJ::statushistory_add($u, undef, 'pay_modify',
|
|
"deactivating bonus feature due to paid account expiration: item=$item; ".
|
|
"saving $new_daysleft extra days");
|
|
|
|
# return a list of deactivated rows
|
|
push @deactivated, { 'item' => $item, 'size' => $size, 'daysleft' => $new_daysleft };
|
|
}
|
|
|
|
return @deactivated;
|
|
}
|
|
|
|
# returns 1 on success, undef on error
|
|
# - bonus_ref: opt, reference in which to return output of LJ::Pay::freeze_bonus
|
|
# - perm: opt, set to remove permanent status
|
|
sub LJ::Pay::remove_paid_account {
|
|
my ($userid, $bonus_ref, $perm) = @_;
|
|
my $u = ref $userid ? $userid : LJ::load_userid($userid, "force");
|
|
return undef unless $u;
|
|
|
|
# remove paid user cap
|
|
{
|
|
my @cap_remove = 'paid';
|
|
push @cap_remove, 'perm' if $perm;
|
|
|
|
LJ::modify_caps($u, [], [ @cap_remove ])
|
|
or return undef;
|
|
}
|
|
|
|
# delete paiduser/email alias rows
|
|
my $dbh = LJ::get_db_writer();
|
|
$dbh->do("DELETE FROM paiduser WHERE userid=?", undef, $u->{'userid'});
|
|
$dbh->do("DELETE FROM email_aliases WHERE alias=?", undef, "$u->{'user'}\@$LJ::USER_DOMAIN")
|
|
unless exists $LJ::FIXED_ALIAS{$u->{'user'}};
|
|
|
|
# note the transition for stats
|
|
LJ::Pay::update_paytrans($userid, 'paidaccount', 'X')
|
|
or return undef;
|
|
|
|
# log this paid account expiration
|
|
my $name = $perm ? "perm" : "paid";
|
|
LJ::statushistory_add($u, undef, 'pay_modify', "expiring $name account");
|
|
|
|
# returns list/hash of item => { paidexp row }
|
|
@$bonus_ref = LJ::Pay::freeze_bonus($u);
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub LJ::Pay::is_valid_cart {
|
|
my $cartobj = shift;
|
|
|
|
# do some checks on the cart to make sure that it is valid/intact?
|
|
my $dbh;
|
|
|
|
# iterate over all items and make sure that each one is allowed to be there
|
|
my %done = ();
|
|
my $found_coppa = undef;
|
|
foreach my $it (@{$cartobj->{'items'}}) {
|
|
|
|
# cache that we checked this userid, item combination
|
|
{
|
|
my $key = "$it->{'rcptid'}-$it->{'item'}";
|
|
next if $done{$key};
|
|
$done{$key} = 1;
|
|
}
|
|
|
|
# run checks for 'sized' bonus item types
|
|
if (LJ::Pay::is_bonus($it, 'sized')) {
|
|
return undef unless LJ::Pay::can_apply_sized_bonus($it->{'rcptid'}, $cartobj, $it->{'item'});
|
|
|
|
# run checks for 'bool' bonus item types
|
|
} elsif (LJ::Pay::is_bonus($it, 'bool')) {
|
|
return undef unless LJ::Pay::can_apply_bool_bonus($it->{'rcptid'}, $cartobj, $it->{'item'});
|
|
|
|
# check for attempted use of already used coupons
|
|
} elsif ($it->{'item'} eq 'coupon' && $it->{'amt'} < 0) {
|
|
$dbh ||= LJ::get_db_writer();
|
|
my $payid = $dbh->selectrow_array("SELECT payid FROM coupon WHERE cpid=?",
|
|
undef, $it->{'tokenid'});
|
|
return undef unless $payid && $payid == $cartobj->{'payid'};
|
|
|
|
|
|
} elsif ($it->{'item'} eq 'coppa') {
|
|
return undef if $found_coppa;
|
|
return undef unless $it->{'rcptid'};
|
|
|
|
my $rcpt = LJ::load_userid($it->{'rcptid'});
|
|
return undef unless $rcpt->{userid} == $cartobj->{userid} && $rcpt->underage;
|
|
|
|
$found_coppa = 1;
|
|
}
|
|
}
|
|
|
|
if ($cartobj->{userid} && ! $found_coppa) {
|
|
my $u = LJ::load_userid($cartobj->{userid}) or return undef; # invalid user on cart
|
|
|
|
# no coppa found and cart owner is underage
|
|
return undef if $u->underage;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub LJ::Pay::get_bool_bonus_price {
|
|
my ($item, $qty) = @_;
|
|
|
|
# allow passing of an $it hash
|
|
if (ref $item eq 'HASH') {
|
|
$qty = $item->{'qty'};
|
|
$item = $item->{'item'};
|
|
}
|
|
|
|
return undef unless $item && $qty && LJ::Pay::is_bonus($item, 'bool');
|
|
|
|
return $LJ::Pay::bonus{$item}->{'items'}->{$qty}->{'amount'};
|
|
}
|
|
|
|
sub LJ::Pay::get_sized_bonus_price {
|
|
my ($u, $cartobj, $item, $size, $qty) = @_;
|
|
|
|
my $userid = LJ::want_userid($u);
|
|
|
|
# allow passing of an $it hash
|
|
if (ref $item eq 'HASH') {
|
|
# get size from subitem
|
|
$size = (split("-", $item->{'subitem'}))[0];
|
|
$qty = $item->{'qty'};
|
|
$item = $item->{'item'};
|
|
}
|
|
|
|
# easy/obvious checks
|
|
return undef unless $userid && LJ::Pay::is_bonus($item, 'sized') && $size > 0;
|
|
|
|
# total price of this item with no comp
|
|
my $total_price = $LJ::Pay::bonus{$item}->{'items'}->{$size}->{'qty'}->{$qty}->{'amount'};
|
|
|
|
# no negative prices allowed
|
|
$total_price = 0 if $total_price < 0;
|
|
|
|
# if there is already an item of this size in the cart, it already received a comp, so don't do it again
|
|
return $total_price
|
|
if grep { $_->{'rcptid'} == $userid && $_->{'item'} eq $item &&
|
|
(split("-", $_->{'subitem'}))[0] == $size } @{$cartobj->{'items'}};
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
my $row = $dbh->selectrow_hashref("SELECT TO_DAYS(expdate)-TO_DAYS(NOW()) AS 'curr_days', " .
|
|
"TO_DAYS(NOW() + INTERVAL ? MONTH)-TO_DAYS(NOW()) AS 'new_days', " .
|
|
"size AS 'curr_size' FROM paidexp WHERE userid=? AND item=?",
|
|
undef, $qty, $userid, $item);
|
|
$row->{'new_size'} = $size;
|
|
$row->{'curr_days'} = 0 if $row->{'curr_days'} < 0;
|
|
|
|
# if current size is what they're trying to buy or there are no current days, there is no comp'ing to be done
|
|
return $total_price if $row->{'curr_size'} == $row->{'new_size'} || $row->{'curr_days'} == 0;
|
|
|
|
# find areas of new/existing rectangles to be bought
|
|
my $old_area = $row->{'curr_size'} * $row->{'curr_days'};
|
|
my $new_area = $row->{'new_size'} * $row->{'new_days'};
|
|
my $rate = $old_area / ($new_area || 1);
|
|
|
|
# calculate comp'd price to subtract from the total
|
|
my $comp_amt = $total_price * $rate;
|
|
|
|
# return final price to user.
|
|
my $final_price = sprintf("%.02f", $total_price - $comp_amt);
|
|
|
|
# don't let final price be < 0
|
|
$final_price = 0 if $final_price < 0;
|
|
|
|
return $final_price;
|
|
}
|
|
|
|
# return list of bonus items available for purchase,
|
|
# to be plugged into LJ::html_select()
|
|
sub LJ::Pay::bonus_item_list {
|
|
my ($u, $cartobj) = @_; # purchasing user
|
|
|
|
my @bool;
|
|
my @sized;
|
|
foreach my $itemname (keys %LJ::Pay::bonus) {
|
|
my $bitem = $LJ::Pay::bonus{$itemname};
|
|
next unless ref $bitem eq 'HASH' && ref $bitem->{'items'} eq 'HASH'; # eh?
|
|
|
|
# bool type
|
|
if ($bitem->{'type'} eq 'bool') {
|
|
foreach my $qty (sort { $b <=> $a } keys %{$bitem->{'items'}}) {
|
|
my $amt = $bitem->{'items'}->{$qty}->{'amount'};
|
|
push @bool, ("$itemname-$qty",
|
|
LJ::Pay::product_name($itemname, undef, $qty) . " (\$$amt.00 USD)");
|
|
}
|
|
|
|
next;
|
|
}
|
|
|
|
# sized type
|
|
if ($u && $bitem->{'type'} eq 'sized') {
|
|
foreach my $size (reverse sort { $a <=> $b } keys %{$bitem->{'items'}}) {
|
|
my $sizeit = $bitem->{'items'}->{$size};
|
|
foreach my $qty (sort { $b <=> $a } keys %{$sizeit->{'qty'}}) {
|
|
|
|
# do a bunch of checks, $u probably is $remote since gifts aren't allowed
|
|
next unless LJ::Pay::can_apply_sized_bonus($u, $cartobj, $itemname, $size, $qty);
|
|
|
|
# will be interpretted as item-subitem-qty
|
|
my $amt = $sizeit->{'qty'}->{$qty}->{'amount'};
|
|
my $amt_comp = LJ::Pay::get_sized_bonus_price($u, $cartobj, $itemname, $size, $qty);
|
|
push @sized, ("$itemname-$size-$qty",
|
|
LJ::Pay::product_name($itemname, $size, $qty) . " (\$$amt.00 USD" .
|
|
($amt == $amt_comp ? "" : "; to upgrade: \$$amt_comp") . ")");
|
|
}
|
|
}
|
|
|
|
next;
|
|
}
|
|
}
|
|
|
|
return (@bool, @sized);
|
|
}
|
|
|
|
sub LJ::Pay::postal_address_text {
|
|
return join("\n", @LJ::PAY_POSTAL_ADDRESS, @_);
|
|
}
|
|
|
|
sub LJ::Pay::postal_address_html {
|
|
return join('<br />', @LJ::PAY_POSTAL_ADDRESS, @_);
|
|
}
|
|
|
|
sub LJ::Pay::account_summary {
|
|
my $u = shift;
|
|
return undef unless $u;
|
|
|
|
# find account name
|
|
my $acctname;
|
|
my $eff_cap;
|
|
foreach my $capkey (sort { $LJ::Pay::capinf{$a}->{'bit'} <=> $LJ::Pay::capinf{$b}->{'bit'} } keys %LJ::Pay::capinf) {
|
|
my $cap = $LJ::Pay::capinf{$capkey};
|
|
next unless $u->{'caps'} & 1 << $cap->{'bit'};
|
|
$acctname = $cap->{'name'};
|
|
$eff_cap = $capkey;
|
|
}
|
|
|
|
# get paid account expiration date, but only if the user is paid (not perm)
|
|
my $dbh = LJ::get_db_writer();
|
|
my $paid_exp;
|
|
if ($eff_cap eq 'paid') {
|
|
$paid_exp = $dbh->selectrow_array("SELECT paiduntil FROM paiduser WHERE userid=? " .
|
|
"AND paiduntil > NOW()",
|
|
undef, $u->{'userid'});
|
|
}
|
|
|
|
my $trim = sub { return substr($_[0], 0, 10); };
|
|
|
|
my $ret;
|
|
|
|
# account type
|
|
$ret .= "<ul>";
|
|
$ret .= "<li><b>$acctname</b>";
|
|
$ret .= " - expiring <i>" . $trim->($paid_exp) . "</i>" if $paid_exp;
|
|
$ret .= "</li>";
|
|
|
|
# bonus features
|
|
my $sth = $dbh->prepare("SELECT * FROM paidexp WHERE userid=? AND (expdate>NOW() OR daysleft>0)");
|
|
$sth->execute($u->{'userid'});
|
|
my $bonus = "<ul>";
|
|
my $ct;
|
|
while (my $exp = $sth->fetchrow_hashref) {
|
|
|
|
$bonus .= "<li>" . LJ::Pay::product_name($exp, "short") . " - ";
|
|
|
|
# enabled has 2 cases:
|
|
# - item has associated cap and it's enabled on $u
|
|
# - daysleft == 0, meaning expdate > NOW() by query above
|
|
my $bit = $LJ::Pay::bonus{$exp->{'item'}}->{'cap'};
|
|
my $has_cap = $u->{'caps'} & 1 << $bit;
|
|
my $enabled = defined $bit && $has_cap || $exp->{'daysleft'} == 0;
|
|
|
|
# active
|
|
if ($enabled) {
|
|
$bonus .= "Active, expiring <i>" . $trim->($exp->{'expdate'}) . "</i>";
|
|
|
|
# inactive
|
|
} else {
|
|
$bonus .= "Inactive, $exp->{'daysleft'} days remaining";
|
|
}
|
|
$bonus .= "</li>";
|
|
|
|
$ct++;
|
|
}
|
|
$ret .= "$bonus</ul>" if $ct;
|
|
|
|
$ret .= "</ul>";
|
|
|
|
return $ret;
|
|
}
|
|
|
|
sub LJ::Pay::quota_summary {
|
|
my $u = shift;
|
|
|
|
# disk quota usage
|
|
my $diskquota = LJ::Blob::get_disk_usage($u);
|
|
my $diskmax = LJ::get_cap($u, "disk_quota") * 1024**2;
|
|
return undef unless $diskquota && $diskmax;
|
|
|
|
my $size = sub {
|
|
my $bytes = shift;
|
|
|
|
# print in mb
|
|
return sprintf("%.2f MiB", $bytes / 1024**2)
|
|
if $bytes > 1024**2;
|
|
|
|
# print in k
|
|
return sprintf("%.2fkb", $bytes / 1024);
|
|
};
|
|
|
|
my $pct = sub {
|
|
return sprintf("%.2f%%", ($_[0] / $_[1]) * 100);
|
|
};
|
|
|
|
my $ret;
|
|
|
|
$ret .= "<ul>";
|
|
$ret .= "<li>Total: " . $size->($diskquota) . " of " . $size->($diskmax);
|
|
$ret .= " (" . $pct->($diskquota, $diskmax) . ")</li>";
|
|
|
|
$ret .= "<ul>";
|
|
my @blobtypes = (['userpic', "Userpics"], ['phonepost', 'PhonePost']);
|
|
push @blobtypes, ['fotobilder', 'Photo Hosting'] if LJ::get_cap($u, 'fb_account');
|
|
foreach (@blobtypes) {
|
|
my ($domain, $name) = @$_;
|
|
|
|
my $used = LJ::Blob::get_disk_usage($u, $domain);
|
|
$ret .= "<li>$name: " . $size->($used);
|
|
$ret .= " (" . $pct->($used, $diskmax) . ")</li>";
|
|
}
|
|
$ret .= "</ul></ul>";
|
|
|
|
return $ret;
|
|
|
|
};
|
|
|
|
sub LJ::Pay::render_cart {
|
|
my $cartobj = shift;
|
|
|
|
my $ret = shift;
|
|
my $opts = shift;
|
|
|
|
my $remote = LJ::get_remote();
|
|
|
|
$$ret .= <<HDR;
|
|
<table width='95%' cellpadding='2'>
|
|
<tr>
|
|
<td width='1%'></td>
|
|
<td width='30%'><b><u>Item</u></b></td>
|
|
<td width='30%'><b><u>Type</u></b></td>
|
|
<td width='30%'><b><u>Recipient</u></b></td>
|
|
<td width='9%' align='right'><b><u>Amount</u></b></td>
|
|
</tr>
|
|
HDR
|
|
|
|
my $is_items = 0;
|
|
unless ($cartobj && @{$cartobj->{'items'}}) {
|
|
$$ret .= "<tr><td colspan='5'><i>(no items)</i></tr>";
|
|
} else {
|
|
$is_items = 1;
|
|
my %name = (
|
|
'paidacct' => 'Paid Account',
|
|
'coupon' => "Coupon",
|
|
'perm' => 'Permanent Account',
|
|
'rename' => 'Rename Token',
|
|
'shipping' => "Shipping Cost",
|
|
'diskquota' => "Disk Quota",
|
|
'userpic' => "Extra Userpics",
|
|
'coppa' => "Age Verification (for COPPA)",
|
|
);
|
|
|
|
# load user objects & pics
|
|
my (%user, %pic);
|
|
foreach my $it (@{$cartobj->{'items'}}) {
|
|
next unless $it->{'rcptid'};
|
|
$user{$it->{'rcptid'}} = \$user{$it->{'rcptid'}};
|
|
}
|
|
LJ::load_userids_multiple([ %user ], [ $remote ]);
|
|
if ($opts->{'pics'}) {
|
|
LJ::load_userpics(\%pic, [ map { [ $_, $_->{'defaultpicid'} ] } values %user ]);
|
|
}
|
|
|
|
my $ljuopts = {};
|
|
$ljuopts->{'imgroot'} = "$LJ::SSLROOT/img" if $opts->{'secureimg'};
|
|
|
|
foreach my $it (sort { $b->{'amt'} <=> $a->{'amt'} } @{$cartobj->{'items'}}) {
|
|
my $size;
|
|
|
|
$$ret .= "<tr valign='top'><td>";
|
|
$$ret .= LJ::html_check({ 'type' => 'check', 'name' => "del_$it->{'piid'}",
|
|
'id' => "del_$it->{'piid'}", 'disabled' => $it->{'item'} eq 'coppa' })
|
|
if $opts->{'remove'};
|
|
$$ret .= "</td><td>";
|
|
|
|
# default item name
|
|
my $name = $name{$it->{'item'}} || $it->{'item'};
|
|
|
|
# bonus features
|
|
if (LJ::Pay::is_bonus($it)) {
|
|
$name = LJ::Pay::product_name($it, "short");
|
|
}
|
|
|
|
# clothing items
|
|
if ($it->{'item'} eq "clothes") {
|
|
my ($style, $col, $sz) = split(/-/, $it->{'subitem'});
|
|
if ($LJ::Pay::product{"clothes-$style"}) {
|
|
$name = $LJ::Pay::product{"clothes-$style"}->[0];
|
|
}
|
|
$name ||= "Unknown Clothing: $name";
|
|
$name .= ", " . $LJ::Pay::color{$col};
|
|
$name = "<b>$name</b>" if $opts->{shipping_labels};
|
|
if ($opts->{'pics'}) {
|
|
$name .= "<br /><img src=\"$LJ::IMGPREFIX/tshirts/thumb/$style-$col.jpg\" width='200' height='191' />";
|
|
}
|
|
$size = $LJ::Pay::size{$sz}->[1];
|
|
|
|
}
|
|
|
|
# discount coupons
|
|
if ($it->{'item'} eq "coupon") {
|
|
$name = LJ::Pay::product_name($it->{'item'}, $it->{'subitem'});
|
|
$name .= "<br />(<tt>$it->{'token'}</tt>)" if $it->{'token'} && $it->{'amt'} < 0;
|
|
}
|
|
$$ret .= "<label for='del_$it->{'piid'}'>$name</label>";
|
|
|
|
# is this an anonymous gift?
|
|
if ($it->{'anon'}) {
|
|
$$ret .= ", anonymous";
|
|
}
|
|
|
|
# is there a delivery date?
|
|
if ($it->{'giveafter'}) {
|
|
$$ret .= ", to be delivered ";
|
|
my @gmt = gmtime($it->{'giveafter'});
|
|
$$ret .= sprintf("%04d-%02d-%02d %02d:%02d",
|
|
$gmt[5]+1900, $gmt[4]+1, $gmt[3],
|
|
$gmt[2], $gmt[1]);
|
|
}
|
|
|
|
# if this is being called from an admin page, optionally show tokens associated
|
|
# with renames, coupons, etc.
|
|
if ($opts->{'tokens'} && $it->{'token'} ne "") {
|
|
my $token = $it->{'token'};
|
|
if ($it->{'item'} eq "paidacct" || $it->{'item'} eq "perm") {
|
|
$token = "<a href='/admin/codetrace.bml?code=$token'>$token</a>";
|
|
}
|
|
|
|
# link between payid that bought
|
|
# the coupon and payid which used the coupon
|
|
if ($it->{'item'} eq "coupon") {
|
|
my $dbh = LJ::get_db_writer();
|
|
my ($payid, $ppayid) =
|
|
$dbh->selectrow_array("SELECT payid, ppayid FROM coupon WHERE cpid=?",
|
|
undef, $it->{'tokenid'});
|
|
|
|
my $id = $it->{'amt'} < 0 ? $ppayid : $payid;
|
|
$token = "<a href='paiddetails.bml?payid=$id'>$token</a>" if $id;
|
|
}
|
|
|
|
if ($it->{'item'} eq "rename") {
|
|
my $dbh = LJ::get_db_writer();
|
|
my ($from, $to, $date) =
|
|
$dbh->selectrow_array("SELECT fromuser, touser, rendate FROM renames " .
|
|
"WHERE payid=? AND renid=?",
|
|
undef, $it->{'payid'}, $it->{'tokenid'});
|
|
if ($from && $to && $date) {
|
|
$token .= "<br />" .
|
|
LJ::ljuser($from, { 'no_follow' => 1 }) . " => " .
|
|
LJ::ljuser($to, { 'no_follow' => 1 }) . "<br />@ $date";
|
|
} else {
|
|
$token .= " (unused)";
|
|
}
|
|
}
|
|
|
|
$$ret .= "<br /><b><tt>$token</tt></b>";
|
|
}
|
|
if ($opts->{'piids'}) {
|
|
$$ret .= "<br /><small>[piid: $it->{'piid'} {$it->{'status'}}]</small>";
|
|
}
|
|
$$ret .= "</td><td>";
|
|
|
|
# item type column
|
|
if ($it->{'item'} eq "paidacct" ||
|
|
defined $LJ::Pay::bonus{$it->{'item'}}) {
|
|
|
|
$$ret .= "$it->{'qty'} months";
|
|
} elsif ($it->{'item'} eq "clothes") {
|
|
$$ret .= $opts->{shipping_labels} ? "<b>$size</b>" : $size;
|
|
} elsif ($it->{'item'} eq "coupon") {
|
|
$$ret .= "\$$it->{'amt'} USD";
|
|
}
|
|
|
|
$$ret .= "</td><td>";
|
|
if ($it->{'rcptid'}) {
|
|
my $u = $user{$it->{'rcptid'}};
|
|
$$ret .= LJ::ljuser($u->{'user'}, $ljuopts) . " - " . LJ::ehtml($u->{'name'});
|
|
if ($u->{'defaultpicid'} && $opts->{'pics'}) {
|
|
my $p = $pic{$u->{'defaultpicid'}};
|
|
$$ret .= "<br /><img src='$LJ::USERPIC_ROOT/$u->{'defaultpicid'}/$u->{'userid'}' width='$p->{'width'}' height='$p->{'height'}'>";
|
|
}
|
|
|
|
} else {
|
|
$$ret .= "<nobr>" . LJ::ehtml($it->{'rcptemail'}) . "</nobr>";
|
|
}
|
|
$$ret .= "</td><td align='right'>";
|
|
$$ret .= sprintf("\$%0.02f", $it->{'amt'});
|
|
$$ret .= "</td></tr>";
|
|
}
|
|
|
|
}
|
|
|
|
# analyze various amounts in this cart
|
|
my $amts = LJ::Pay::coupon_reduce($cartobj);
|
|
|
|
# print dollars
|
|
my $damt = sub { sprintf("\$%.02f", shift()) };
|
|
|
|
if ($amts->{'cp_used_tot'} > 0) {
|
|
$$ret .= "<tr><td colspan='4' align='right' valign='top'>Subtotal:</td>";
|
|
$$ret .= "<td align='right'>" . $damt->($amts->{'cart_amt_tot'}) . "</td></tr>";
|
|
if ($amts->{'cp_used_gen'} > 0) {
|
|
$$ret .= "<tr><td colspan='4' align='right' valign='top'>General Coupon:</td>";
|
|
$$ret .= "<td align='right'>" . $damt->(-$amts->{'cp_used_gen'}) . "</td></tr>";
|
|
}
|
|
if ($amts->{'cp_used_int'} > 0) {
|
|
$$ret .= "<tr><td colspan='4' align='right' valign='top'>Intangible Coupon:</td>";
|
|
$$ret .= "<td align='right'>" . $damt->(-$amts->{'cp_used_int'}) . "</td></tr>";
|
|
}
|
|
if ($amts->{'cp_used_tan'} > 0) {
|
|
$$ret .= "<tr><td colspan='4' align='right' valign='top'>Tangible Coupon:</td>";
|
|
$$ret .= "<td align='right'>" . $damt->(-$amts->{'cp_used_tan'}) . "</td></tr>";
|
|
}
|
|
}
|
|
$$ret .= "<tr><td colspan='4' align='right' valign='top'><b>Total (USD):</b></td>";
|
|
$$ret .= "<td align='right'>" . $damt->($amts->{'adj_amt_tot'}) . "</td></tr>";
|
|
|
|
if ($opts->{'remove'} || ($opts->{'checkout'} && LJ::Pay::can_checkout_cart($cartobj))) {
|
|
|
|
# warning of coupons in cart which are not fully utilized
|
|
if ($amts->{'cp_unused_gen'} > 0) {
|
|
$$ret .= "<tr valign='top'><td colspan='5' align='left'>";
|
|
$$ret .= "<b>Warning:</b> You are using " . $damt->($amts->{'cp_amt_gen'});
|
|
$$ret .= " worth of general-purpose coupons on this order. However, ";
|
|
$$ret .= $damt->($amts->{'cp_unused_gen'}) . " of that is currently unused. ";
|
|
$$ret .= "If you choose to check out now, the " . $damt->($amts->{'cp_unused_gen'});
|
|
$$ret .= " will be wasted!";
|
|
$$ret .= "</td></tr>\n";
|
|
}
|
|
|
|
# warning of intangible coupons in cart which are not fully utilized
|
|
if ($amts->{'cp_unused_int'} > 0) {
|
|
$$ret .= "<tr valign='top'><td colspan='5' align='left'>";
|
|
$$ret .= "<b>Warning:</b> You are using " . $damt->($amts->{'cp_amt_int'});
|
|
$$ret .= " worth of coupons which are <i>only</i> valid for intangible ";
|
|
$$ret .= "purchases such as Paid Accounts and Bonus Features. However, ";
|
|
$$ret .= $damt->($amts->{'cp_unused_int'}) . " of that is currently unused. ";
|
|
$$ret .= "If you choose to check out now, the " . $damt->($amts->{'cp_unused_int'});
|
|
$$ret .= " will be wasted!";
|
|
$$ret .= "</td></tr>\n";
|
|
}
|
|
|
|
# warning of tangible coupons in cart which are not fully utilized
|
|
if ($amts->{'cp_unused_tan'} > 0) {
|
|
$$ret .= "<tr valign='top'><td colspan='5' align='left'>";
|
|
$$ret .= "<b>Warning:</b> You are using " . $damt->($amts->{'cp_amt_tan'});
|
|
$$ret .= " worth of coupons which are <i>only</i> valid for tangible ";
|
|
$$ret .= "purchases such as Tee Shirts and Hoodies. However, ";
|
|
$$ret .= $damt->($amts->{'cp_unused_tan'}) . " of that is currently unused. ";
|
|
$$ret .= "If you choose to check out now, the " . $damt->($amts->{'cp_unused_tan'});
|
|
$$ret .= " will be wasted!";
|
|
$$ret .= "</td></tr>\n";
|
|
}
|
|
|
|
if (grep { $_->{'item'} eq 'rename' || $_->{'item'} eq 'coupon' && $_->{'amt'} > 0 }
|
|
@{$cartobj->{'items'}}) {
|
|
|
|
$$ret .= "<tr valign='top'><td colspan='5' align='left'>";
|
|
$$ret .= "<b>AOL Users:</b> To prevent difficulties receiving your rename token ";
|
|
$$ret .= "and/or coupon, please adjust your email settings to allow email from ";
|
|
$$ret .= "$LJ::ACCOUNTS_EMAIL.</td></tr>\n";
|
|
}
|
|
|
|
my $has_coppa = LJ::Pay::cart_contains_coppa($cartobj);
|
|
if ($has_coppa) {
|
|
$$ret .= "<tr valign='top'><td colspan='5' align='left'>";
|
|
$$ret .= "<b>Note:</b> Because your cart contains a <a href='$LJ::SITEROOT/legal/coppa.bml'>COPPA</a> Verification item, ";
|
|
$$ret .= "you must pay for this cart via credit card.</td></tr>\n";
|
|
}
|
|
|
|
my $disabled = $is_items ? "" : "disabled='disabled'";
|
|
$$ret .= "<tr valign='top'><td colspan='2'>";
|
|
if ($opts->{'remove'}) {
|
|
$$ret .= "<input type='submit' name='action:removesel' $disabled value='Remove Selected'>";
|
|
}
|
|
$$ret .= "</td><td colspan='4' align='right'>";
|
|
if ($opts->{'checkout'} && LJ::Pay::can_checkout_cart($cartobj)) {
|
|
|
|
if ($amts->{'adj_amt_tot'} > 0 || $has_coppa || LJ::Pay::cart_needs_shipping($cartobj)) {
|
|
$$ret .= "Payment method: ";
|
|
|
|
my @pay_list = (cc => "Credit Card");
|
|
|
|
# if the cart contains a special "coppa" item, then the only vaid method of payment
|
|
# is credit card.
|
|
unless ($has_coppa) {
|
|
push @pay_list, ( "paypal" => "PayPal",
|
|
"check" => "Check",
|
|
"moneyorder" => "Money Order",
|
|
"cash" => "Cash", );
|
|
}
|
|
|
|
$$ret .= LJ::html_select({'name' => 'paymeth', }, @pay_list);
|
|
$$ret .= "\n<input type='submit' name='action:checkout' value='Check out -->'>";
|
|
|
|
} else {
|
|
$$ret .= "No charge. <input type='submit' name='action:checkout' value='Check out -->'>";
|
|
}
|
|
}
|
|
$$ret .= "</td></tr>\n";
|
|
}
|
|
|
|
$$ret .= "</table>";
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub revoke_payitems
|
|
{
|
|
my $dbh = LJ::get_db_writer();
|
|
foreach my $it (@_) {
|
|
next unless $it->{'status'} eq "done";
|
|
|
|
# revoke a rename token
|
|
if ($it->{'item'} eq "rename") {
|
|
|
|
# the rename token might've been used already, in which case it's kinda
|
|
# too late. but we'll mark the rename as refunded regardless, so maybe
|
|
# later we can write something to flop things back... we'll see.
|
|
$dbh->do("UPDATE renames SET token='----------' WHERE renid=?", undef,
|
|
$it->{'tokenid'});
|
|
|
|
# log to statushistory, but only if there's a userid to associate it with
|
|
LJ::statushistory_add($it->{'rcptid'}, undef, "revoke", "revoking rename token: $it->{'token'}")
|
|
if $it->{'rcptid'};
|
|
|
|
next;
|
|
}
|
|
|
|
# revoke a paid or permanent account
|
|
if ($it->{'item'} eq "paidacct" || $it->{'item'} eq "perm") {
|
|
my $revid = $it->{'rcptid'};
|
|
if ($it->{'tokenid'}) {
|
|
$dbh->do("UPDATE acctcode SET auth='-----' WHERE acid=?", undef,
|
|
$it->{'tokenid'});
|
|
# but maybe the code wasn't used yet, so let's check and
|
|
# cancel the revoke, since we stopped it in time
|
|
$revid = $dbh->selectrow_array("SELECT rcptid FROM acctcode WHERE ".
|
|
"acid=?", undef, $it->{'tokenid'});
|
|
}
|
|
my $months = $it->{'item'} eq "perm" ? 99 : $it->{'qty'};
|
|
LJ::Pay::remove_paid_months($revid, $months, $it) if $revid;
|
|
next;
|
|
}
|
|
|
|
# revoke a tangible shipping item
|
|
if (LJ::Pay::item_needs_shipping($it)) {
|
|
# delete from shipping so this shipping label won't show up anymore
|
|
$dbh->do("DELETE FROM shipping WHERE payid=?", undef, $it->{'payid'});
|
|
|
|
# log to statushistory, but only if there's a userid to associate it with
|
|
LJ::statushistory_add($it->{'rcptid'}, undef, "revoke",
|
|
"revoking shipping item: " . LJ::Pay::product_name($it))
|
|
if $it->{'rcptid'};
|
|
|
|
next;
|
|
}
|
|
|
|
# revoke bonus features
|
|
if (LJ::Pay::is_bonus($it)) {
|
|
|
|
# this function will handle messiness and log to statushistory
|
|
LJ::Pay::remove_bonus_item($it);
|
|
next;
|
|
}
|
|
}
|
|
|
|
my $in = join(',', map { $_->{'piid'}+0 } @_);
|
|
$dbh->do("UPDATE payitems SET status='refund' WHERE piid IN ($in)") if $in;
|
|
}
|
|
|
|
sub LJ::Pay::remove_bonus_item {
|
|
my $it = shift;
|
|
return undef unless ref $it eq 'HASH';
|
|
return undef unless LJ::Pay::is_bonus($it);
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
# update paidexp
|
|
$dbh->do("UPDATE paidexp SET expdate=expdate - INTERVAL ? MONTH " .
|
|
"WHERE userid=? AND item=?",
|
|
undef, $it->{'qty'}, $it->{'rcptid'}, $it->{'item'});
|
|
|
|
# if they're now totally out of time for this bonus feature, we need to
|
|
# delete their paidexp row altogether and possibly remove their cap
|
|
|
|
my $newrow = $dbh->selectrow_hashref("SELECT *, (expdate>NOW()) AS 'timeleft' " .
|
|
"FROM paidexp WHERE userid=? AND item=?",
|
|
undef, $it->{'rcptid'}, $it->{'item'});
|
|
|
|
unless ($newrow->{'timeleft'}) {
|
|
|
|
# delete empty row
|
|
$dbh->do("DELETE FROM paidexp WHERE userid=? AND item=? AND expdate<NOW()",
|
|
undef, $it->{'rcptid'}, $it->{'item'});
|
|
|
|
# update user's cap if necessary
|
|
my $cap = $LJ::Pay::bonus{$it->{'item'}}->{'cap'};
|
|
LJ::modify_caps($it->{'rcptid'}, [], [$cap]) if $cap;
|
|
}
|
|
|
|
# log to statushistory
|
|
LJ::statushistory_add($it->{'rcptid'}, undef, 'revoke',
|
|
LJ::Pay::product_name($it, "shoort") . "; $it->{'qty'} months");
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub bazaar_do_expirations {
|
|
my $uid = shift;
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
my $userclause;
|
|
if ($uid) {
|
|
$uid += 0;
|
|
$userclause = "userid=$uid AND ";
|
|
}
|
|
|
|
# do expirations
|
|
$dbh->do("UPDATE bzrbalance SET expired=owed, owed=0 WHERE $userclause ".
|
|
"owed > 0 AND date < DATE_SUB(NOW(), INTERVAL 93 DAY)");
|
|
}
|
|
|
|
sub bazaar_remove_balance {
|
|
my ($u, $amt) = @_;
|
|
my $dbh = LJ::get_db_writer();
|
|
return 0 unless $u;
|
|
|
|
my $key = "bzrbaldecr-$u->{'userid'}";
|
|
my $r = $dbh->selectrow_array("SELECT GET_LOCK(?, 3)", undef, $key);
|
|
return 0 unless $r;
|
|
my $unlock = sub {
|
|
$dbh->selectrow_array("SELECT RELEASE_LOCK(?)", undef, $key);
|
|
};
|
|
|
|
LJ::Pay::bazaar_do_expirations($u->{'userid'});
|
|
|
|
my @owed;
|
|
my $bal;
|
|
my $sth = $dbh->prepare("SELECT bzid, owed FROM bzrbalance ".
|
|
"WHERE userid=? AND owed > 0 ORDER BY date");
|
|
$sth->execute($u->{'userid'});
|
|
while (my ($bzid, $owed) = $sth->fetchrow_array) {
|
|
push @owed, [ $bzid, $owed ];
|
|
$bal += $owed;
|
|
}
|
|
|
|
if ($bal < $amt) {
|
|
$unlock->();
|
|
return 0;
|
|
}
|
|
|
|
my $remain = $amt;
|
|
while ($remain >= 0.01 && @owed) {
|
|
my $rec = shift @owed;
|
|
my $remove = $rec->[1] < $remain ? $rec->[1] : $remain;
|
|
my $rv = $dbh->do("UPDATE bzrbalance SET owed=GREATEST(0,owed-?) WHERE userid=? AND bzid=?",
|
|
undef, $remove, $u->{'userid'}, $rec->[0]);
|
|
$remain -= $remove if $rv;
|
|
}
|
|
|
|
$unlock->();
|
|
return 1;
|
|
}
|
|
|
|
sub new_coupon {
|
|
my ($type, $amt, $rcptid, $ppayid) = @_;
|
|
|
|
my $dbh = LJ::get_db_writer() or return undef;
|
|
my $auth = LJ::make_auth_code(10);
|
|
$dbh->do("INSERT INTO coupon (auth, type, arg, rcptid, ppayid) " .
|
|
"VALUES (?, ?, ?, ?, ?)", undef, $auth, $type, $amt, $rcptid, $ppayid);
|
|
return undef if $dbh->err;
|
|
|
|
my $tokenid = $dbh->{'mysql_insertid'};
|
|
return ($tokenid, "$tokenid-$auth");
|
|
}
|
|
|
|
# to be called as &nodb; (so this function sees caller's @_)
|
|
sub nodb {
|
|
shift @_ if
|
|
ref $_[0] eq "LJ::DBSet" || ref $_[0] eq "DBI::db" ||
|
|
ref $_[0] eq "DBIx::StateKeeper" || ref $_[0] eq "Apache::DBI::db";
|
|
}
|
|
|
|
|
|
1;
|