#!/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 # # 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.'); }