414 lines
13 KiB
Text
414 lines
13 KiB
Text
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# bin/worker/paidstatus
|
||
|
|
#
|
||
|
|
# Worker job that loops and looks for shopping carts that are in the need of
|
||
|
|
# processing.
|
||
|
|
#
|
||
|
|
# Authors:
|
||
|
|
# Mark Smith <mark@dreamwidth.org>
|
||
|
|
#
|
||
|
|
# Copyright (c) 2009 by Dreamwidth Studios, LLC.
|
||
|
|
#
|
||
|
|
# This program is free software; you may redistribute it and/or modify it under
|
||
|
|
# the same terms as Perl itself. For a copy of the license, please reference
|
||
|
|
# 'perldoc perlartistic' or 'perldoc perlgpl'.
|
||
|
|
#
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
|
||
|
|
BEGIN {
|
||
|
|
require "$ENV{LJHOME}/cgi-bin/ljlib.pl";
|
||
|
|
}
|
||
|
|
|
||
|
|
use Time::HiRes qw/ gettimeofday tv_interval /;
|
||
|
|
use LJ::Sendmail;
|
||
|
|
use LJ::Lang;
|
||
|
|
use DW::Shop;
|
||
|
|
use DW::Shop::Cart;
|
||
|
|
use DW::Pay;
|
||
|
|
|
||
|
|
################################################################################
|
||
|
|
## main setup
|
||
|
|
################################################################################
|
||
|
|
|
||
|
|
# setup logging routine
|
||
|
|
my $begin_time = [ gettimeofday() ];
|
||
|
|
my ( $logfile, $last_log_time );
|
||
|
|
my $log = sub {
|
||
|
|
$last_log_time ||= [ gettimeofday() ];
|
||
|
|
|
||
|
|
unless ($logfile) {
|
||
|
|
open $logfile, ">>$LJ::HOME/logs/paidstatus.log"
|
||
|
|
or die "Internal server error creating log.\n";
|
||
|
|
print $logfile "[0.00s 0.00s] Log started at " . LJ::mysql_time( gmtime() ) . ".\n";
|
||
|
|
}
|
||
|
|
|
||
|
|
my $fmt = "[%0.4fs %0.1fs] " . shift() . "\n";
|
||
|
|
my $msg = sprintf( $fmt, tv_interval($last_log_time), tv_interval($begin_time), @_ );
|
||
|
|
|
||
|
|
# now log to both the file and STDERR if we're foregrounded
|
||
|
|
print $logfile $msg;
|
||
|
|
print STDERR $msg;
|
||
|
|
|
||
|
|
$last_log_time = [ gettimeofday() ];
|
||
|
|
};
|
||
|
|
|
||
|
|
# setup alert routine, this sends a mail to some configurable alert address
|
||
|
|
my $alert = sub {
|
||
|
|
LJ::send_mail(
|
||
|
|
{
|
||
|
|
to => $LJ::PAYPAL_CONFIG{email},
|
||
|
|
from => $LJ::BOGUS_EMAIL,
|
||
|
|
subject => "$LJ::SITENAME Payment System Alert",
|
||
|
|
body => shift(),
|
||
|
|
}
|
||
|
|
);
|
||
|
|
return undef;
|
||
|
|
};
|
||
|
|
|
||
|
|
while (1) {
|
||
|
|
$log->('Main loop beginning...');
|
||
|
|
|
||
|
|
# do this in a sub so it can return on error
|
||
|
|
main_loop();
|
||
|
|
|
||
|
|
# now we sleep to the next one minute boundary, and if we're taking more
|
||
|
|
# than one minute to run, we fire off an alert
|
||
|
|
my $sleep_time = 10 - tv_interval($begin_time);
|
||
|
|
if ( $sleep_time <= 0 ) {
|
||
|
|
$alert->('Warning: main loop is taking longer than a minute.');
|
||
|
|
$sleep_time = 10;
|
||
|
|
}
|
||
|
|
$log->( 'Sleeping for %0.2f seconds.', $sleep_time );
|
||
|
|
select undef, undef, undef, $sleep_time;
|
||
|
|
|
||
|
|
$log->('Main loop ended.');
|
||
|
|
$begin_time = [ gettimeofday() ];
|
||
|
|
}
|
||
|
|
|
||
|
|
################################################################################
|
||
|
|
## main loop
|
||
|
|
################################################################################
|
||
|
|
|
||
|
|
sub main_loop {
|
||
|
|
|
||
|
|
# disconnect dbs
|
||
|
|
LJ::DB::disconnect_dbs();
|
||
|
|
LJ::start_request();
|
||
|
|
|
||
|
|
# now get a db or die
|
||
|
|
my $dbh = LJ::get_db_writer()
|
||
|
|
or return $log->('Unable to get database writer handle.');
|
||
|
|
|
||
|
|
## PHASE 0) REMOVE DEAD CARTS (open or closed for more than 30 days)
|
||
|
|
|
||
|
|
my $ct = $dbh->do(
|
||
|
|
q{DELETE FROM shop_carts
|
||
|
|
WHERE state IN (?, ?) AND starttime < UNIX_TIMESTAMP() - 86400 * 30
|
||
|
|
LIMIT 1000},
|
||
|
|
undef, $DW::Shop::STATE_CLOSED, $DW::Shop::STATE_OPEN
|
||
|
|
);
|
||
|
|
return $log->( 'Database error cleaning carts: %s', $dbh->errstr )
|
||
|
|
if $dbh->err;
|
||
|
|
$log->( 'Cleaned %d carts that were unused for more than 30 days.', $ct + 0 );
|
||
|
|
DW::Stats::increment( 'dw.shop.cart.expired', $ct );
|
||
|
|
|
||
|
|
## PHASE 1) PROCESS PAYMENTS
|
||
|
|
|
||
|
|
# dig up carts that are in state paid and scannable
|
||
|
|
my $cartids = $dbh->selectcol_arrayref(
|
||
|
|
q{SELECT cartid FROM shop_carts WHERE state = ? AND nextscan < UNIX_TIMESTAMP()},
|
||
|
|
undef, $DW::Shop::STATE_PAID );
|
||
|
|
return $log->( 'Database error: %s', $dbh->errstr )
|
||
|
|
if $dbh->err;
|
||
|
|
return $log->('Invalid response looking for scannable carts.')
|
||
|
|
unless $cartids && ref $cartids eq 'ARRAY';
|
||
|
|
|
||
|
|
$log->( 'Found %d scannable carts.', scalar(@$cartids) );
|
||
|
|
|
||
|
|
# now iterate over these and do something with them
|
||
|
|
scan_cart( $dbh, $_ ) foreach @$cartids;
|
||
|
|
|
||
|
|
## PHASE 2) PROCESS EXPIRATIONS
|
||
|
|
|
||
|
|
# dig up who has expired ... accounts
|
||
|
|
my $uids = $dbh->selectcol_arrayref(
|
||
|
|
q{SELECT userid FROM dw_paidstatus
|
||
|
|
WHERE permanent = 0 AND (expiretime IS NOT NULL AND expiretime <= UNIX_TIMESTAMP())}
|
||
|
|
);
|
||
|
|
return $log->( 'Database error: %s', $dbh->errstr )
|
||
|
|
if $dbh->err;
|
||
|
|
|
||
|
|
$log->( 'Found %d expired users.', scalar(@$uids) );
|
||
|
|
|
||
|
|
# now expire the user
|
||
|
|
expire_user( $dbh, $_ ) foreach @$uids;
|
||
|
|
|
||
|
|
## PHASE 3) PROCESS EXPIRATION WARNING MAILS
|
||
|
|
|
||
|
|
# dig up who expire soon
|
||
|
|
my $rows = $dbh->selectall_arrayref(
|
||
|
|
q{SELECT userid, lastemail, expiretime - UNIX_TIMESTAMP()
|
||
|
|
FROM dw_paidstatus
|
||
|
|
WHERE permanent = 0 AND
|
||
|
|
(expiretime IS NOT NULL AND
|
||
|
|
expiretime > UNIX_TIMESTAMP() AND
|
||
|
|
expiretime < ( UNIX_TIMESTAMP() + 14*86400 )
|
||
|
|
)}
|
||
|
|
);
|
||
|
|
return $log->( 'Database error: %s', $dbh->errstr )
|
||
|
|
if $dbh->err;
|
||
|
|
|
||
|
|
$log->( 'Found %d users expiring soon.', scalar(@$rows) );
|
||
|
|
|
||
|
|
# now warn the user
|
||
|
|
warn_user( $dbh, $_ ) foreach @$rows;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
sub expire_user {
|
||
|
|
my ( $dbh, $uid ) = @_;
|
||
|
|
|
||
|
|
my $u = LJ::load_userid($uid)
|
||
|
|
or return 0;
|
||
|
|
|
||
|
|
$log->( 'Expiring %s(%d).', $u->user, $u->id );
|
||
|
|
|
||
|
|
if ( $u->is_community && $u->is_visible ) {
|
||
|
|
|
||
|
|
# send an email to every maintainer
|
||
|
|
my $maintus = LJ::load_userids( $u->maintainer_userids );
|
||
|
|
foreach my $maintu ( values %$maintus ) {
|
||
|
|
LJ::send_mail(
|
||
|
|
{
|
||
|
|
to => $maintu->email_raw,
|
||
|
|
fromname => $LJ::SITENAME,
|
||
|
|
from => $LJ::ACCOUNTS_EMAIL,
|
||
|
|
subject => LJ::Lang::ml(
|
||
|
|
"shop.expiration.comm.0.subject",
|
||
|
|
{ sitename => $LJ::SITENAME }
|
||
|
|
),
|
||
|
|
body => LJ::Lang::ml(
|
||
|
|
"shop.expiration.comm.0.body",
|
||
|
|
{
|
||
|
|
touser => $maintu->display_name,
|
||
|
|
commname => $u->display_name,
|
||
|
|
shopurl => "$LJ::SHOPROOT/account?for=gift&user=" . $u->user,
|
||
|
|
sitename => $LJ::SITENAME,
|
||
|
|
}
|
||
|
|
),
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
elsif ( $u->is_visible ) {
|
||
|
|
LJ::send_mail(
|
||
|
|
{
|
||
|
|
to => $u->email_raw,
|
||
|
|
fromname => $LJ::SITENAME,
|
||
|
|
from => $LJ::ACCOUNTS_EMAIL,
|
||
|
|
subject =>
|
||
|
|
LJ::Lang::ml( "shop.expiration.user.0.subject", { sitename => $LJ::SITENAME } ),
|
||
|
|
body => LJ::Lang::ml(
|
||
|
|
"shop.expiration.user.0.body",
|
||
|
|
{
|
||
|
|
touser => $u->display_name,
|
||
|
|
shopurl => "$LJ::SHOPROOT/account?for=self",
|
||
|
|
sitename => $LJ::SITENAME,
|
||
|
|
}
|
||
|
|
),
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
# this is pretty easy, we just tell DW::Pay to do it
|
||
|
|
return DW::Pay::expire_user($uid);
|
||
|
|
}
|
||
|
|
|
||
|
|
sub warn_user {
|
||
|
|
my ( $dbh, $row ) = @_;
|
||
|
|
my ( $uid, $lastmail, $timeleft ) = @$row;
|
||
|
|
|
||
|
|
my $u = LJ::load_userid($uid)
|
||
|
|
or return 0;
|
||
|
|
return 0 unless $u->is_visible;
|
||
|
|
|
||
|
|
my $mail;
|
||
|
|
if ( $timeleft < 86400 * 3 && ( !defined $lastmail || $lastmail == 14 ) ) {
|
||
|
|
$log->( 'Sending 3-day expiration mail to %s(%d).', $u->user, $u->id );
|
||
|
|
$mail = '3';
|
||
|
|
|
||
|
|
}
|
||
|
|
elsif ( $timeleft < 86400 * 14 && !defined $lastmail ) {
|
||
|
|
$log->( 'Sending 14-day expiration mail to %s(%d).', $u->user, $u->id );
|
||
|
|
$mail = '14';
|
||
|
|
}
|
||
|
|
|
||
|
|
return 1 unless defined $mail;
|
||
|
|
|
||
|
|
DW::Stats::increment( 'dw.shop.paid_account.warn_' . $mail, 1 );
|
||
|
|
|
||
|
|
# alter warning message body for premium paid accounts
|
||
|
|
my $bodytype = $mail;
|
||
|
|
my $status = DW::Pay::get_account_type($u);
|
||
|
|
$bodytype = "$status.$mail" if $status eq "premium";
|
||
|
|
|
||
|
|
if ( $u->is_community ) {
|
||
|
|
|
||
|
|
# send an email to every maintainer
|
||
|
|
my $maintus = LJ::load_userids( $u->maintainer_userids );
|
||
|
|
foreach my $maintu ( values %$maintus ) {
|
||
|
|
LJ::send_mail(
|
||
|
|
{
|
||
|
|
to => $maintu->email_raw,
|
||
|
|
fromname => $LJ::SITENAME,
|
||
|
|
from => $LJ::ACCOUNTS_EMAIL,
|
||
|
|
subject => LJ::Lang::ml(
|
||
|
|
"shop.expiration.comm.$mail.subject",
|
||
|
|
{ sitename => $LJ::SITENAME }
|
||
|
|
),
|
||
|
|
body => LJ::Lang::ml(
|
||
|
|
"shop.expiration.comm.$bodytype.body",
|
||
|
|
{
|
||
|
|
touser => $maintu->display_name,
|
||
|
|
commname => $u->display_name,
|
||
|
|
shopurl => "$LJ::SHOPROOT/account?for=gift&user=" . $u->user,
|
||
|
|
sitename => $LJ::SITENAME,
|
||
|
|
}
|
||
|
|
),
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
LJ::send_mail(
|
||
|
|
{
|
||
|
|
to => $u->email_raw,
|
||
|
|
fromname => $LJ::SITENAME,
|
||
|
|
from => $LJ::ACCOUNTS_EMAIL,
|
||
|
|
subject => LJ::Lang::ml(
|
||
|
|
"shop.expiration.user.$mail.subject",
|
||
|
|
{ sitename => $LJ::SITENAME }
|
||
|
|
),
|
||
|
|
body => LJ::Lang::ml(
|
||
|
|
"shop.expiration.user.$bodytype.body",
|
||
|
|
{
|
||
|
|
touser => $u->display_name,
|
||
|
|
shopurl => "$LJ::SHOPROOT/account?for=self",
|
||
|
|
sitename => $LJ::SITENAME,
|
||
|
|
}
|
||
|
|
),
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
# now update the db
|
||
|
|
$dbh->do( 'UPDATE dw_paidstatus SET lastemail = ? WHERE userid = ?', undef, $mail + 0, $u->id );
|
||
|
|
return 0
|
||
|
|
if $dbh->err;
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub scan_cart {
|
||
|
|
my $dbh = shift;
|
||
|
|
my $cartid = shift() + 0;
|
||
|
|
|
||
|
|
# easy sub for setting nextscan on this cart
|
||
|
|
my $nextscan = sub {
|
||
|
|
$dbh->do( q{UPDATE shop_carts SET nextscan = UNIX_TIMESTAMP() + ? WHERE cartid = ?},
|
||
|
|
undef, shift() || 3600, $cartid );
|
||
|
|
$log->( 'Database error: %s', $dbh->errstr )
|
||
|
|
if $dbh->err;
|
||
|
|
return 1;
|
||
|
|
};
|
||
|
|
|
||
|
|
# setup a failure sub, this will log and alert on errors plus mark the cart as
|
||
|
|
# not being scannable for another hour
|
||
|
|
my $fail = sub {
|
||
|
|
my $msg = 'scan_cart(%d): ' . shift();
|
||
|
|
$msg = sprintf( $msg, $cartid, @_ );
|
||
|
|
|
||
|
|
$log->($msg);
|
||
|
|
$alert->($msg);
|
||
|
|
|
||
|
|
return undef;
|
||
|
|
};
|
||
|
|
|
||
|
|
# prepend our logging function with useful information
|
||
|
|
my $log = sub {
|
||
|
|
$log->( 'scan_cart(%d): ' . shift(), $cartid, @_ );
|
||
|
|
};
|
||
|
|
|
||
|
|
$log->( '-' x 60 );
|
||
|
|
|
||
|
|
my $cart = DW::Shop::Cart->get_from_cartid($cartid);
|
||
|
|
return $fail->('Failed creating cart.')
|
||
|
|
unless $cart && ref $cart eq 'DW::Shop::Cart';
|
||
|
|
|
||
|
|
# error check this cart
|
||
|
|
return $fail->('Cart not in a valid state.')
|
||
|
|
unless $cart->state == $DW::Shop::STATE_PAID;
|
||
|
|
return $fail->('Cart has no items.')
|
||
|
|
unless $cart->has_items;
|
||
|
|
|
||
|
|
# try to apply each item
|
||
|
|
my ( $unapplied, %saw_ids ) = (0);
|
||
|
|
$log->('Iterating over items.');
|
||
|
|
foreach my $item ( @{ $cart->items } ) {
|
||
|
|
next unless $item->apply_automatically;
|
||
|
|
|
||
|
|
$log->( 'Found item [%d] %s.', $item->id, $item->short_desc );
|
||
|
|
|
||
|
|
# rare case where we've found the cart generating items with the same
|
||
|
|
# id, leading to failures in sending invite codes
|
||
|
|
while ( exists $saw_ids{ $item->id } ) {
|
||
|
|
if ( $item->applied ) {
|
||
|
|
$log->('Item id duplicate, but item safely applied. Ignoring dupe id.');
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
# this item has NOT been applied, so renumber it
|
||
|
|
$item->id( $item->id + 1 );
|
||
|
|
$log->( 'Item id already found, renumbering to %d.', $item->id );
|
||
|
|
}
|
||
|
|
|
||
|
|
# record the id in our list so we know we've seen it
|
||
|
|
$saw_ids{ $item->id } = 1;
|
||
|
|
|
||
|
|
# this is the normal 'bail' point for already applied items
|
||
|
|
if ( $item->applied ) {
|
||
|
|
$log->('Item already applied.');
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
# try to apply it
|
||
|
|
my $rv = eval { $item->apply };
|
||
|
|
if ($rv) {
|
||
|
|
$log->('Successfully applied item.');
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$log->( 'Failed to apply item: %s', DW::Pay::error_text() || $@ || 'unknown error' );
|
||
|
|
$unapplied = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# yes, we save the cart a lot... oh well
|
||
|
|
$cart->save( no_memcache => 1 );
|
||
|
|
}
|
||
|
|
|
||
|
|
# two possible results: we have items still unapplied or we did
|
||
|
|
# get everything applied. try again in 1-2 hours.
|
||
|
|
if ($unapplied) {
|
||
|
|
$nextscan->( 3600 + int( rand() * 3600 ) );
|
||
|
|
$log->('One or more items not applied, will retry later.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
# everything in this order has been applyed, restate it
|
||
|
|
$cart->state( $DW::Shop::STATE_PROCESSED, no_memcache => 1 );
|
||
|
|
|
||
|
|
# main loop done!
|
||
|
|
$log->('Cart->state is now PROCESSED.');
|
||
|
|
}
|