#!/usr/bin/perl # # DW::Pay # # Core of the payment system. # # Authors: # Mark Smith # # Copyright (c) 2008-2013 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'. # package DW::Pay; use strict; use v5.10; use Log::Log4perl; my $log = Log::Log4perl->get_logger(__PACKAGE__); use Carp qw/ confess /; use HTTP::Request; use LWP::UserAgent; use DW::BusinessRules::Pay; use DW::Task::SphinxCopier; our $error_code = undef; our $error_text = undef; use constant ERR_FATAL => 1; use constant ERR_TEMP => 2; ################################################################################ # DW::Pay::type_is_valid # # ARGUMENTS: typeid # # typeid required the id of the type we're checking # # RETURN: 1/0 if the type is a valid type # sub type_is_valid { return 1 if $LJ::CAP{ $_[0] } && $LJ::CAP{ $_[0] }->{_account_type} && $LJ::CAP{ $_[0] }->{_visible_name}; return 0; } ################################################################################ # DW::Pay::type_name # # ARGUMENTS: typeid # # typeid required the id of the type we're checking # # RETURN: string name of type, else undef # sub type_name { confess "invalid typeid $_[0]" unless DW::Pay::type_is_valid( $_[0] ); return $LJ::CAP{ $_[0] }->{_visible_name}; } ################################################################################ # DW::Pay::type_shortname # # ARGUMENTS: typeid # # typeid required the id of the type we're checking # # RETURN: string short name of type, else undef # sub type_shortname { confess "invalid typeid $_[0]" unless DW::Pay::type_is_valid( $_[0] ); return $LJ::CAP{ $_[0] }->{_account_type}; } ################################################################################ # DW::Pay::all_shortnames # # ARGUMENTS: (none) # # RETURN: { typeid => shortname } hashref # sub all_shortnames { my %names; while ( my ( $typeid, $data ) = each %LJ::CAP ) { # Avoid calling DW::Pay::type_is_valid a zillion times for the same typeid $names{$typeid} = $data->{_account_type} if DW::Pay::type_is_valid($typeid); } return \%names; } ################################################################################ # DW::Pay::get_paid_status # # ARGUMENTS: uuserid # # uuserid required user object or userid to get paid status of # # RETURN: Hashref if paid (or has ever been), undef if free: # # { # typeid => ... # expiretime => db time epoch seconds they expire at # expiresin => seconds until they expire # permanent => 1/0 if they're permanent # } # sub get_paid_status { DW::Pay::clear_error(); my ( $uuid, %opts ) = @_; my $uid; my $use_cache = !$opts{no_cache}; $uid = LJ::want_userid($uuid) if defined $uuid; return error( ERR_FATAL, "Invalid user object/userid passed in." ) unless defined $uid && $uid > 0; return $LJ::PAID_STATUS{$uid} if $use_cache && $LJ::PAID_STATUS{$uid}; my $dbr = DW::Pay::get_db_reader() or return error( ERR_TEMP, "Failed acquiring database reader handle." ); my $row = $dbr->selectrow_hashref( q{ SELECT IFNULL(expiretime, 0) - UNIX_TIMESTAMP() AS 'expiresin', typeid, expiretime, permanent FROM dw_paidstatus WHERE userid = ? }, undef, $uid ); return error( ERR_FATAL, "Database error: " . $dbr->errstr ) if $dbr->err; $LJ::PAID_STATUS{$uid} = $row; return $row; } ################################################################################ # DW::Pay::default_typeid # # RETURN: typeid of the default account type. # sub default_typeid { # try to get the default cap class. note that we confess here because # these errors are bad enough to warrant bailing whoever is calling us. my @defaults = grep { $LJ::CAP{$_}->{_account_default} } keys %LJ::CAP; confess 'must have one %LJ::CAP class set _account_default to use the payment system' if scalar(@defaults) < 1; confess 'only one %LJ::CAP class can be set as _account_default' if scalar(@defaults) > 1; # There Can Be Only One return $defaults[0]; } ################################################################################ # DW::Pay::is_default_type # # ARGUMENTS: hashref returned from get_paid_status # # RETURN: 1 if default_typeid should be used, 0 otherwise # sub is_default_type { my $stat = $_[0]; # free accounts: no row, or expired (but not permanent) return 1 unless defined $stat; return 1 unless $stat->{permanent} || $stat->{expiresin} > 0; # use typeid defined in row return 0; } ################################################################################ # DW::Pay::get_current_account_status # # ARGUMENTS: uuserid # # uuserid required user object or userid to get paid status of # # RETURN: typeid from get_paid_status, or else default_typeid # sub get_current_account_status { my $uid = LJ::want_userid( $_[0] ); return DW::Pay::default_typeid() unless $uid; # check memcache first my $memkey = [ $uid, "accttype:$uid" ]; my $typeid = LJ::MemCache::get($memkey); return $typeid if defined $typeid; # try to get current paid status if not in memcache my $stat = DW::Pay::get_paid_status(@_); # default check $typeid = DW::Pay::is_default_type($stat) ? DW::Pay::default_typeid() : $stat->{typeid}; # store in memcache for 15 minutes LJ::MemCache::set( $memkey, $typeid, 900 ); return $typeid; } ################################################################################ # DW::Pay::expire_status_cache # # ARGUMENTS: uuserid # # uuserid required user object or userid to expire status cache of # # RETURN: undef on error, else 1 on success. # sub expire_status_cache { my $uid = LJ::want_userid( $_[0] ); return undef unless $uid; my $memkey = [ $uid, "accttype:$uid" ]; LJ::MemCache::delete($memkey); delete $LJ::PAID_STATUS{$uid}; return 1; } ################################################################################ # DW::Pay::get_account_expiration_time # # ARGUMENTS: uuserid # # uuserid required user object or userid to get paid status of # # RETURN: -1 for free or perm, 0 for expired paid, else the unix timestamp this # account expires on... # # yes, this function has a very weird return value. :( # sub get_account_expiration_time { # try to get current paid status my $stat = DW::Pay::get_paid_status(@_); # free accounts: no row, or expired # perm accounts: no expiration return -1 if !defined $stat || $stat->{permanent}; return 0 unless $stat->{expiresin} > 0; # valid row, return whatever the expiration time is return time() + $stat->{expiresin}; } ################################################################################ # DW::Pay::get_account_type # # ARGUMENTS: uuserid # # uuserid required user object or userid to get paid status of # # RETURN: value defined as _account_type in %LJ::CAP. # sub get_account_type { my $typeid = DW::Pay::get_current_account_status(@_); confess 'account has no valid typeid' unless $typeid && $typeid > 0; confess "typeid $typeid not a valid account level" unless DW::Pay::type_is_valid($typeid); return $LJ::CAP{$typeid}->{_account_type}; } ################################################################################ # DW::Pay::get_refund_points_rate # # ARGUMENTS: uuserid # # uuserid required user object or userid to get paid status of # # RETURN: value defined as _refund_points in %LJ::CAP. # sub get_refund_points_rate { my $typeid = DW::Pay::get_current_account_status(@_); confess 'account has no valid typeid' unless $typeid && $typeid > 0; confess "typeid $typeid not a valid account level" unless DW::Pay::type_is_valid($typeid); return $LJ::CAP{$typeid}->{_refund_points} || 0; } ################################################################################ # DW::Pay::can_refund_points # # ARGUMENTS: uuserid # # uuserid required user object or userid to get refundable status of # # RETURN: 1/0 # sub can_refund_points { my $u = LJ::want_user( $_[0] ); return 0 unless LJ::isu($u); my $secs_since_refund = time() - ( $u->prop("shop_refund_time") || 0 ); return $secs_since_refund > 86400 * 30 ? 1 : 0; } ################################################################################ # DW::Pay::get_account_type_name # # ARGUMENTS: uuserid # # uuserid required user object or userid to get paid status of # # RETURN: value defined as _visible_name in %LJ::CAP. # sub get_account_type_name { my $typeid = DW::Pay::get_current_account_status(@_); confess 'account has no valid typeid' unless $typeid && $typeid > 0; confess "typeid $typeid not a valid account level" unless DW::Pay::type_is_valid($typeid); return $LJ::CAP{$typeid}->{_visible_name}; } ################################################################################ # DW::Pay::get_current_paid_userids # # ARGUMENTS: limit => #rows, typeid => paid account type, permanent => 0|1 # # limit optional how many userids to return (default: no limit) # typeid optional 1 to restrict to basic paid, 2 for premium paid # (default: both) # permanent optional false to restrict to expiring accounts, true to # permanent (default: both) # # RETURN: arrayref of userids for currently paid accounts matching the above # restrictions # sub get_current_paid_userids { DW::Pay::clear_error(); my %opts = @_; my $sql = 'SELECT userid FROM dw_paidstatus WHERE '; my ( @where, @values ); if ( exists $opts{permanent} ) { push @where, 'permanent = ?'; push @values, ( $opts{permanent} ? 1 : 0 ); push @where, 'expiretime > UNIX_TIMESTAMP(NOW())' unless $opts{permanent}; } else { push @where, '(permanent = 1 OR expiretime > UNIX_TIMESTAMP(NOW()))'; } if ( exists $opts{typeid} ) { push @where, 'typeid = ?'; push @values, $opts{typeid}; } $sql .= join ' AND ', @where; if ( exists $opts{limit} ) { $sql .= ' LIMIT ?'; push @values, $opts{limit}; } my $dbr = DW::Pay::get_db_reader() or return error( ERR_TEMP, "Unable to get db reader." ); my $uids = $dbr->selectcol_arrayref( $sql, {}, @values ); return error( ERR_FATAL, "Database error: " . $dbr->errstr ) if $dbr->err; return $uids; } ################################################################################ # DW::Pay::expire_user # # ARGUMENTS: uuserid # # uuserid required user object or userid to set paid status for # # RETURN: undef on error, else 1 on success. # # This is a low level function that expires a user if they need to be. It's a # no-op if the user is not supposed to be expired, but don't call it if you know # that's the case. # sub expire_user { DW::Pay::clear_error(); my ( $u, %opts ) = @_; $u = LJ::want_user($u) or return error( ERR_FATAL, "Invalid/not a user object." ); unless ( $opts{force} ) { my $ps = DW::Pay::get_paid_status($u); return 1 unless $ps; # free already return error( ERR_FATAL, "Cannot expire a permanent account." ) if $ps->{permanent}; return error( ERR_FATAL, "Account not ready for expiration." ) if $ps->{expiresin} > 0; } # so we have to update their status now LJ::statushistory_add( $u, undef, 'paidstatus', 'Expiring account; forced=' . ( $opts{force} ? 1 : 0 ) . '.' ); DW::Pay::update_paid_status( $u, _expire => 1 ); DW::Pay::sync_caps($u); my $rv = eval { # activate also does inactivations $u->activate_userpics; $u->delete_email_alias; 1; }; warn "Failed to perform one or more payment postflight tasks!\n" unless $rv; # happy times DW::Stats::increment( 'dw.shop.paid_account.expired', 1 ); return 1; } ################################################################################ # DW::Pay::add_paid_time # # ARGUMENTS: uuserid, class, months # # uuserid required user object or userid to set paid status for # class required type of account to be using (_account_type) # months required how many months to grant, 99 = perm # days required how many days (in addition to months) to grant # # RETURN: undef on error, else 1 on success. # # This is a low level function, you better be logging! # sub add_paid_time { DW::Pay::clear_error(); my $u = LJ::want_user( shift() ) or return error( ERR_FATAL, "Invalid/not a user object." ); my $type = shift(); my ($typeid) = grep { $LJ::CAP{$_}->{_account_type} && $LJ::CAP{$_}->{_account_type} eq $type } keys %LJ::CAP; return error( ERR_FATAL, 'Invalid type, no typeid found.' ) unless $typeid; my ( $months, $days ) = @_; return error( ERR_FATAL, 'Invalid value for months.' ) unless $months >= 0 && $months <= 99; return error( ERR_FATAL, 'Invalid value for days.' ) unless $days >= 0 && $days <= 31; return error( ERR_FATAL, 'Empty time increment' ) unless $months > 0 || $days > 0; # okay, let's see what the user is right now to decide what to do my $permanent = $months == 99 ? 1 : 0; my ( $newtypeid, $amonths, $adays, $asecs ) = ( $typeid, $months, $days, 0 ); $amonths = 0 if $permanent; # if they have a $ps hashref, they have or had paid time at some point if ( my $ps = DW::Pay::get_paid_status( $u, no_cache => 1 ) ) { # easy bail if they're permanent return error( ERR_FATAL, 'User is already permanent, cannot apply more time.' ) if $ps->{permanent}; # not permanent, but do they have at least a minute left? if ( $ps->{expiresin} > 60 ) { # if it's the same type as what we've got, we just carry it forward by # however much time they have left if ( $ps->{typeid} == $typeid ) { $asecs = $ps->{expiresin}; # but if they're going permanent... } elsif ($permanent) { $asecs = $ps->{expiresin}; # but the types are different... } else { # FIXME: this needs to not be dw-nonfree logic my $from_type = $LJ::CAP{ $ps->{typeid} }->{_account_type}; my $to_type = $LJ::CAP{$typeid}->{_account_type}; # paid->premium, we convert any existing time to premium # ($amonths are already premium and are added later) if ( $from_type eq 'paid' && $to_type eq 'premium' ) { $asecs = DW::BusinessRules::Pay::convert( $from_type, $to_type, undef, undef, $ps->{expiresin} ); # premium->paid, upgrade the new buy to premium. we give them 21 # days of premium time for every month of paid time they were buying. # We also need to convert any day value provided, for use from /admin/pay } elsif ( $from_type eq 'premium' && $to_type eq 'paid' ) { $newtypeid = $ps->{typeid}; # we are not sending their current time to the conversion function # because it is already premium. just convert the newly purchased time. # But, we do include any value in $adays to accomodate arbitrary additions. $asecs = $ps->{expiresin} + DW::BusinessRules::Pay::convert( $to_type, $from_type, $amonths, $adays, undef ); $amonths = 0; $adays = 0; } else { return error( ERR_FATAL, 'Invalid conversion.' ); } } } # at this point we can ignore whatever they have in $ps, as we're going # overwrite it with our own stuff } $asecs += $adays * 86400; # so at this point, we can do whatever we're supposed to do my $rv = DW::Pay::update_paid_status( $u, typeid => $newtypeid, permanent => $permanent, _set_months => $amonths, _add_secs => $asecs, ); # and make sure caps are always in sync DW::Pay::sync_caps($u) if $rv; # the following updates can error, and if they do then we don't want to break the # whole payment flow my $do_postflight = eval { $u->activate_userpics; $u->update_email_alias; 1; }; warn "Failed to perform one or more payment postflight tasks!\n" unless $do_postflight; # all good, we hope :-) return $rv; } ################################################################################ # DW::Pay::update_paid_status # # ARGUMENTS: uuserid, key => value pairs # # uuserid required user object or userid to set paid status for # key required column being set # value required new value to set column to # # RETURN: undef on error, else 1 on success. # # NOTE: this function is a low level function intended to be use for admin # pages and similar functionality. don't use this willy-nilly in anything # else as it is probably not what you want! # # NOTE: you can set special keys if you want to extend time by months, use # _set_months to set expiretime to now + N months, and _add_months to append # that many months. This is more than likely only useful for such things as # admin tools. You may also specify _add_secs if you really want to dig in # and get an exact expiration time. # sub update_paid_status { DW::Pay::clear_error(); my $u = LJ::want_user( shift() ) or return error( ERR_FATAL, "Invalid/not a user object." ); my %cols = (@_) or return error( ERR_FATAL, "Nothing to change!" ); DW::Pay::expire_status_cache( $u->id ); my $dbh = DW::Pay::get_db_writer() or return error( ERR_TEMP, "Unable to get db writer." ); # don't let them add months if the user expired, convert it to set months if ( $cols{_add_months} ) { my $row = DW::Pay::get_paid_status( $u, no_cache => 1 ); if ( $row && $row->{expiresin} > 0 ) { my $time = $dbh->selectrow_array( "SELECT UNIX_TIMESTAMP(DATE_ADD(FROM_UNIXTIME($row->{expiretime}), " . "INTERVAL $cols{_add_months} MONTH))" ); $cols{expiretime} = $time; delete $cols{_add_months}; } else { $cols{_set_months} = delete $cols{_add_months}; } } if ( exists $cols{_set_months} ) { $cols{expiretime} = $dbh->selectrow_array( "SELECT UNIX_TIMESTAMP(DATE_ADD(NOW(), INTERVAL $cols{_set_months} MONTH))"); delete $cols{_set_months}; } if ( exists $cols{_add_secs} ) { $cols{expiretime} += delete $cols{_add_secs}; } return error( ERR_FATAL, "Can't change the userid!" ) if exists $cols{userid}; return error( ERR_FATAL, "Permanent must be 0/1." ) if exists $cols{permanent} && $cols{permanent} !~ /^(?:0|1)$/; return error( ERR_FATAL, "Typeid must be some number and valid." ) if exists $cols{typeid} && !( $cols{typeid} =~ /^(?:\d+)$/ && DW::Pay::type_is_valid( $cols{typeid} ) ); return error( ERR_FATAL, "Expiretime must be some number." ) if exists $cols{expiretime} && $cols{expiretime} !~ /^(?:\d+)$/; return error( ERR_FATAL, "Lastemail must be 0, 3, or 14." ) if exists $cols{lastemail} && defined $cols{lastemail} && $cols{lastemail} !~ /^(?:0|3|14)$/; if ( delete $cols{_expire} ) { $cols{typeid} = DW::Pay::default_typeid(); $cols{lastemail} = undef; $cols{expiretime} = undef; $cols{permanent} = 0; # has to be! } my $cols = join( ', ', map { "$_ = ?" } sort keys %cols ); my @bind = map { $cols{$_} } sort keys %cols; my $ct = $dbh->do( qq{ UPDATE dw_paidstatus SET $cols WHERE userid = ? }, undef, @bind, $u->id ); return error( ERR_FATAL, "Database error: " . $dbh->errstr ) if $dbh->err; # if we got 0 rows edited, we have to insert a new row if ( $ct == 0 ) { # fail if we don't have some valid values return error( ERR_FATAL, "Typeid must be some number and valid." ) unless $cols{typeid} =~ /^(?:\d+)$/ && DW::Pay::type_is_valid( $cols{typeid} ); # now try the insert $dbh->do( q{INSERT INTO dw_paidstatus (userid, typeid, expiretime, permanent, lastemail) VALUES (?, ?, ?, ?, ?)}, undef, $u->id, $cols{typeid}, $cols{expiretime}, $cols{permanent} + 0, $cols{lastemail} ); return error( ERR_FATAL, "Database error: " . $dbh->errstr ) if $dbh->err; } # and now, at this last step, we kick off a job to check if this user # needs to have their search index setup/messed with. if (@LJ::SPHINX_SEARCHD) { DW::TaskQueue->dispatch( DW::Task::SphinxCopier->new( { userid => $u->id, source => "paidstat" } ) ); } return 1; } ################################################################################ # DW::Pay::edit_expiration_datetime # # ARGUMENTS: uuserid, expiration datetime # # uuserid required user object or userid to set paid status for # datetime required new expiration datetime # # RETURN: undef on error, else 1 on success. # # sub edit_expiration_datetime { DW::Pay::clear_error(); my $u = LJ::want_user( shift() ) or return error( ERR_FATAL, "Invalid/not a user object." ); my $datetime = shift(); my $ps = DW::Pay::get_paid_status( $u, no_cache => 1 ); return error( ERR_FATAL, "Can't set expiration date for this type of account" ) if $ps->{expiresin} <= 0 || $ps->{permanent}; my $dbh = DW::Pay::get_db_writer() or return error( ERR_TEMP, "Unable to get db writer." ); my $row = $dbh->selectrow_hashref( "SELECT UNIX_TIMESTAMP(?) AS datetime, ? < NOW() AS expired", undef, $datetime, $datetime ); return error( ERR_FATAL, "Invalid expiration date/time" ) unless $row->{datetime}; return error( ERR_FATAL, "Expiration date/time is in the past" ) if $row->{expired}; return error( ERR_FATAL, "Expiration date/time is unchanged" ) if $row->{datetime} == $ps->{expiretime}; $dbh->do( q{UPDATE dw_paidstatus SET expiretime=? WHERE userid=?}, undef, $row->{datetime}, $u->id ); return error( ERR_FATAL, "Database error: " . $dbh->errstr ) if $dbh->err; DW::Pay::expire_status_cache( $u->id ); return 1; } ################################################################################ # DW::Pay::num_permanent_accounts_available # # ARGUMENTS: none # # RETURN: number of permanent accounts that are still available for purchase # -1 if there is no limit on how many permanent accounts can be # purchased # sub num_permanent_accounts_available { DW::Pay::clear_error(); return 0 unless $LJ::PERMANENT_ACCOUNT_LIMIT; return -1 if $LJ::PERMANENT_ACCOUNT_LIMIT < 0; # 1. figure out how many permanent accounts have been purchased # try memcache first my $ct = LJ::MemCache::get('numpermaccts'); unless ( defined $ct ) { # not in memcache, so let's hit the database # FIXME: add ddlockd so we don't hit the db in waves every 60 seconds my $dbh = DW::Pay::get_db_writer() or return error( ERR_TEMP, "Unable to get db writer." ); $ct = $dbh->selectrow_array('SELECT COUNT(*) FROM dw_paidstatus WHERE permanent = 1') + 0; LJ::MemCache::set( 'numpermaccts', $ct, 60 ); } # 2. figure out how many are left to purchase my $num_available = $LJ::PERMANENT_ACCOUNT_LIMIT - $ct; return $num_available > 0 ? $num_available : 0; } ################################################################################ # DW::Pay::num_permanent_accounts_available_estimated # # ARGUMENTS: none # # RETURN: estimated number of permanent accounts that are still available for # purchase # -1 if there is no limit on how many permanent accounts can be # purchased # sub num_permanent_accounts_available_estimated { my $num_available = DW::Pay::num_permanent_accounts_available(); return $num_available if $num_available < 1; return 10 if $num_available <= 10; return 25 if $num_available <= 25; return 50 if $num_available <= 50; return 100 if $num_available <= 100; return 150 if $num_available <= 150; return 200 if $num_available <= 200; return 300 if $num_available <= 300; return 400 if $num_available <= 400; return 500; } ################################################################################ # DW::Pay::get_random_active_free_user # # ARGUMENTS: journaltype = type (user 'P' or community 'C') of the requested # user ('P' if not given) # for_u = user that is requesting the random free user (remote if # no user is given) # # RETURN: a random active free user that for_u can purchase a paid account for, # or undef if there aren't any valid results # sub get_random_active_free_user { my $journaltype = shift || 'P'; my $for_u = shift || LJ::get_remote(); my $dbr = LJ::get_db_reader(); my $rows = $dbr->selectall_arrayref( q{SELECT userid, points FROM users_for_paid_accounts WHERE journaltype = ? ORDER BY RAND() LIMIT 10}, { Slice => {} }, $journaltype ); my @active_us; my $us = LJ::load_userids( map { $_->{userid} } @$rows ); foreach my $row (@$rows) { my $userid = $row->{userid}; my $points = $row->{points}; my $u = $us->{$userid}; next unless $u && $u->is_visible; next if $u->is_paid; next unless $u->opt_randompaidgifts; next if LJ::sysban_check( 'pay_user', $u->user ); if ( $journaltype eq 'P' ) { next if $for_u && $u->equals($for_u); next if $for_u && $u->has_banned($for_u); } # each point that a user has gives them an extra chance of being chosen out of the array push @active_us, $u; if ($points) { foreach my $point ( 1 .. $points ) { push @active_us, $u; } } } return undef unless scalar @active_us; my @shuffled_us = List::Util::shuffle(@active_us); return $shuffled_us[0]; } ################################################################################ ################################################################################ ################################################################################ # this internal method takes a user's paid status (which is the accurate record # of what caps and things a user should have) and then updates their caps. i.e., # this method is used to make the user's actual caps reflect reality. sub sync_caps { my $u = LJ::want_user(shift) or return error( ERR_FATAL, "Must provide a user to sync caps for." ); my $ps = DW::Pay::get_paid_status($u); # calculate list of caps that we care about my @bits = grep { $LJ::CAP{$_}->{_account_type} } keys %LJ::CAP; my $default = DW::Pay::default_typeid(); # either they're free, or they expired (not permanent) if ( DW::Pay::is_default_type($ps) ) { # reset back to the default, and turn off all other bits; then set the # email count to defined-but-0 $u->modify_caps( [$default], [ grep { $_ != $default } @bits ] ); DW::Pay::update_paid_status( $u, lastemail => 0 ); } else { # this is a really bad error we should never have... we can't # handle this user # FIXME: candidate for email-site-admins return error( ERR_FATAL, "Unknown typeid." ) unless DW::Pay::type_is_valid( $ps->{typeid} ); # simply modify it to use the typeid specified, as typeids are bits... but # turn off any other bits $u->modify_caps( [ $ps->{typeid} ], [ grep { $_ != $ps->{typeid} } @bits ] ); DW::Pay::update_paid_status( $u, lastemail => undef ); } return 1; } sub error { $DW::Pay::error_code = $_[0] + 0; $DW::Pay::error_text = $_[1] || "Unknown error."; return undef; } sub error_code { return $DW::Pay::error_code; } sub error_text { return $DW::Pay::error_text; } sub was_error { return defined $DW::Pay::error_code; } sub clear_error { $DW::Pay::error_code = $DW::Pay::error_text = undef; } sub get_db_reader { # we always use the master, but perhaps we want to use a specific role for # payments later? so we abstracted this... return LJ::get_db_writer(); } sub get_db_writer { return LJ::get_db_writer(); } # return whether we're allowed to buy something for another user # retuning an error if we can't. sub validate_target_user { my ( $target_u, $remote ) = @_; return { error => 'widget.shopitemoptions.error.invalidusername' } unless LJ::isu($target_u); return { error => 'widget.shopitemoptions.error.expungedusername' } if $target_u->is_expunged; return { error => 'widget.shopitemoptions.error.banned' } if $remote && $target_u->has_banned($remote); return { success => 1 }; } sub for_self { my ( $remote, $item_data ) = @_; if ( $remote && $remote->is_personal ) { $item_data->{target_userid} = $remote->id; } else { return error_ml('widget.shopitemoptions.error.notloggedin'); } } sub for_gift { my ( $remote, $target, $errors, $item_data ) = @_; my $target_u = LJ::load_user($target); my $user_check = validate_target_user( $target_u, $remote ); if ( defined $user_check->{error} ) { $errors->add( 'username', $user_check->{error} ); } else { $item_data->{target_userid} = $target_u->id; } } sub validate_deliverydate { my ( $deliverydate, $errors, $item_data ) = @_; $deliverydate =~ /(\d{4})-(\d{2})-(\d{2})/; my $given_date = DateTime->new( year => $1, month => $2, day => $3, ); my $time_check = DateTime->compare( $given_date, DateTime->today ); if ( $time_check < 0 ) { # we were given a date in the past $errors->add_string( 'deliverydate', 'time cannot be in the past' ); #FIXME } elsif ( $time_check > 0 ) { # date is in the future, add it. $item_data->{deliverydate} = $given_date->date; } } 1;