#!/usr/bin/perl # # DW::Shop::Item::Account # # Represents a paid account that someone is purchasing. # # Authors: # Mark Smith # Janine 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'. # package DW::Shop::Item::Account; use base 'DW::Shop::Item'; use strict; use DateTime; use DW::InviteCodes; use DW::Pay; =head1 NAME DW::Shop::Item::Account - Represents a paid account that someone is purchasing. See the documentation for DW::Shop::Item for usage examples and description of methods inherited from that base class. =head1 API =head2 C<< $class->new( [ %args ] ) >> Instantiates an account of some sort to be purchased. Arguments: =item ( see DW::Shop::Item ), =item months => number of months of paid time, =item class => type of paid account, =item random => 1 (if gifting paid time to a random user), =item anonymous_target => 1 (if random user should be anonymous, not identified) =cut # override sub new { my ( $class, %args ) = @_; if ( $args{anonymous_target} ) { return undef unless $args{anonymous_target} == 1; } if ( $args{random} ) { return undef unless $args{random} == 1; } my $self = $class->SUPER::new(%args); if ($self) { $self->{months} = $LJ::SHOP{ $self->{type} }->[1]; $self->{class} = $LJ::SHOP{ $self->{type} }->[2]; } return $self; } # override sub _apply { my $self = $_[0]; return $self->_apply_email if $self->t_email; return $self->_apply_userid if $self->t_userid; # something weird, just kill this item! $self->{applied} = 1; return 1; } # internal application sub, do not call sub _apply_userid { my $self = $_[0]; return 1 if $self->applied; # will need this later my $fu = LJ::load_userid( $self->from_userid ); unless ( $self->anonymous || $self->from_name || $fu ) { warn "Failed to apply: NOT anonymous, no from_name, no from_user!\n"; return 0; } # need this user my $u = LJ::load_userid( $self->t_userid ) or return 0; # try to add the paid time to the user LJ::statushistory_add( $u->id, $self->from_userid, 'paidstatus', sprintf( 'Order #%d: applied %d months of %s.', $self->cartid, $self->months, $self->class_name ) ); DW::Pay::add_paid_time( $u, $self->class, $self->months ) or return 0; { # By definition, things from anonymous purchasers are gifts. my @tags = ( 'gift:' . ( $fu && $fu->equals($u) ? 'no' : 'yes' ), 'anonymous:' . ( $self->anonymous ? 'yes' : 'no' ), 'type:' . $self->class, 'target:account' ); DW::Stats::increment( 'dw.shop.paid_account.applied', 1, [ @tags, 'months:' . $self->months ] ); DW::Stats::increment( 'dw.shop.paid_account.applied_months', $self->months, [@tags] ); } # we're applied now, regardless of what happens with the email $self->{applied} = 1; # look up the account's new expiration date my $expdate; my $paid_status = DW::Pay::get_paid_status($u); if ( $paid_status && !$self->permanent ) { my $exptime = DateTime->from_epoch( epoch => $paid_status->{expiretime} ); $expdate = $exptime->ymd; } # now we have to mail this code my ( $body, $subj ); my $accounttype_string = $self->permanent ? LJ::Lang::ml( 'shop.email.accounttype.permanent', { type => $self->class_name } ) : LJ::Lang::ml( 'shop.email.accounttype', { type => $self->class_name, nummonths => $self->months } ); $subj = LJ::Lang::ml( "shop.email.acct.subject", { sitename => $LJ::SITENAME } ); if ( $u->is_community ) { my $maintus = LJ::load_userids( $u->maintainer_userids ); foreach my $maintu ( values %$maintus ) { my $emailtype = $fu && $maintu->equals($fu) ? 'self' : 'other'; $emailtype = 'anon' if $self->anonymous; $emailtype = 'explicit' if $self->from_name; $body = LJ::Lang::ml( "shop.email.acct.body.start", { touser => $maintu->display_name } ); $body .= "\n"; $body .= LJ::Lang::ml( "shop.email.comm.$emailtype", { fromuser => $fu ? $fu->display_name : '', commname => $u->display_name, sitename => $LJ::SITENAME, fromname => $self->from_name, } ); $body .= LJ::Lang::ml( "shop.email.acct.body.type", { accounttype => $accounttype_string } ); $body .= "\n"; $body .= LJ::Lang::ml( "shop.email.acct.body.expires", { date => $expdate } ) if $expdate; $body .= "\n"; $body .= LJ::Lang::ml( "shop.email.acct.body.note", { reason => $self->reason } ) if $self->reason; $body .= LJ::Lang::ml("shop.email.comm.close"); $body .= LJ::Lang::ml( "shop.email.acct.body.end", { sitename => $LJ::SITENAME } ); # send the email to the maintainer LJ::send_mail( { to => $maintu->email_raw, from => $LJ::ACCOUNTS_EMAIL, fromname => $LJ::SITENAME, subject => $subj, body => $body } ); } } else { my $emailtype; if ( $self->random ) { $emailtype = $self->anonymous ? 'random_anon' : 'random'; } else { $emailtype = $fu && $u->equals($fu) ? 'self' : 'other'; $emailtype = 'anon' if $self->anonymous; $emailtype = 'explicit' if $self->from_name; } $body = LJ::Lang::ml( "shop.email.acct.body.start", { touser => $u->display_name } ); $body .= "\n"; $body .= LJ::Lang::ml( "shop.email.user.$emailtype", { fromuser => $fu ? $fu->display_name : '', sitename => $LJ::SITENAME, fromname => $self->from_name, } ); $body .= LJ::Lang::ml( "shop.email.acct.body.type", { accounttype => $accounttype_string } ); $body .= "\n"; $body .= LJ::Lang::ml( "shop.email.acct.body.expires", { date => $expdate } ) if $expdate; $body .= "\n"; $body .= LJ::Lang::ml( "shop.email.acct.body.note", { reason => $self->reason } ) if $self->reason; $body .= LJ::Lang::ml("shop.email.user.close"); $body .= LJ::Lang::ml( "shop.email.acct.body.end", { sitename => $LJ::SITENAME } ); # send the email to the user LJ::send_mail( { to => $u->email_raw, from => $LJ::ACCOUNTS_EMAIL, fromname => $LJ::SITENAME, subject => $subj, body => $body } ); } # tell the caller we're happy return 1; } # internal application sub, do not call sub _apply_email { my $self = $_[0]; return 1 if $self->applied; # will need this later my $fu = LJ::load_userid( $self->from_userid ); unless ( $self->anonymous || $fu ) { warn "Failed to apply: NOT anonymous and no from_user!\n"; return 0; } { # By definition, things sent to email are gifts. my @tags = ( 'gift:yes', 'target:email', 'anonymous:' . ( $self->anonymous ? 'yes' : 'no' ), 'type:' . $self->class ); DW::Stats::increment( 'dw.shop.paid_account.applied', 1, [ @tags, 'months:' . $self->months ] ); DW::Stats::increment( 'dw.shop.paid_account.applied_months', $self->months, [@tags] ); } my $reason = join ':', 'payment', $self->class, $self->months; my ($code) = DW::InviteCodes->generate( reason => $reason ); my ($acid) = DW::InviteCodes->decode($code); # store in the db my $dbh = LJ::get_db_writer() or return 0; $dbh->do( 'INSERT INTO shop_codes (acid, cartid, itemid) VALUES (?, ?, ?)', undef, $acid, $self->cartid, $self->id ); return 0 if $dbh->err; # now we have to mail this code my ( $body, $subj ); my $accounttype_string = $self->permanent ? LJ::Lang::ml( 'shop.email.accounttype.permanent', { type => $self->class_name } ) : LJ::Lang::ml( 'shop.email.accounttype', { type => $self->class_name, nummonths => $self->months } ); my $emailtype = $self->anonymous ? 'anon' : 'other'; $subj = LJ::Lang::ml( "shop.email.acct.subject", { sitename => $LJ::SITENAME } ); $body = LJ::Lang::ml( "shop.email.acct.body.start", { touser => $self->t_email } ); $body .= "\n"; $body .= LJ::Lang::ml( "shop.email.email.$emailtype", { fromuser => $fu ? $fu->display_name : '', sitename => $LJ::SITENAME, } ); $body .= LJ::Lang::ml( "shop.email.acct.body.type", { accounttype => $accounttype_string } ); $body .= LJ::Lang::ml( "shop.email.acct.body.create", { createurl => "$LJ::SITEROOT/create?code=$code" } ); $body .= LJ::Lang::ml( "shop.email.acct.body.note", { reason => $self->reason } ) if $self->reason; $body .= LJ::Lang::ml("shop.email.email.close"); $body .= LJ::Lang::ml( "shop.email.acct.body.end", { sitename => $LJ::SITENAME } ); # send the email to the user my $rv = LJ::send_mail( { to => $self->t_email, from => $LJ::ACCOUNTS_EMAIL, fromname => $LJ::SITENAME, subject => $subj, body => $body } ); # if this worked, then we're applied! yay! if ($rv) { $self->{applied} = 1; return 1; } # else ... something naughty happened :( warn "Failed to send email!\n"; return 0; } # override sub unapply { my $self = $_[0]; return unless $self->applied; # do the application process now, and if it succeeds... $self->{applied} = 0; warn "$self->{class} unapplied $self->{months} months\n"; return 1; } # override sub can_be_added { my ( $self, %opts ) = @_; my $errref = $opts{errref}; my $target_u = LJ::load_userid( $self->t_userid ); # the receiving user must be a personal or community account if ( LJ::isu($target_u) && !$target_u->is_personal && !$target_u->is_community ) { $$errref = LJ::Lang::ml('shop.item.account.canbeadded.invalidjournaltype'); return 0; } # check to see if we're over the permanent account limit if ( $self->permanent && DW::Pay::num_permanent_accounts_available() < 1 ) { $$errref = LJ::Lang::ml('shop.item.account.canbeadded.noperms'); return 0; } # check to make sure that the target user is valid: not deleted / suspended, etc if ( !$opts{user_confirmed} && LJ::isu($target_u) && $target_u->is_inactive ) { $$errref = LJ::Lang::ml( 'shop.item.account.canbeadded.notactive', { user => $target_u->ljuser_display } ); return 0; } # check to make sure the target user's current account type doesn't conflict with the item if ( LJ::isu($target_u) ) { my $account_type = DW::Pay::get_account_type($target_u); if ( $account_type eq 'seed' ) { # no paid time can be purchased for seed accounts $$errref = LJ::Lang::ml( 'shop.item.account.canbeadded.alreadyperm', { user => $target_u->ljuser_display } ); return 0; } elsif ( !DW::Shop::Item::Account->allow_account_conversion( $target_u, $self->class ) ) { # premium accounts can't get normal paid time $$errref = LJ::Lang::ml( 'shop.item.account.canbeadded.nopaidforpremium', { user => $target_u->ljuser_display } ) if $self->class eq 'paid'; # paid accounts can't get premium time as a gift $$errref = LJ::Lang::ml( 'shop.item.account.canbeadded.nopremiumforpaid', { user => $target_u->ljuser_display } ) if $self->class eq 'premium'; return 0; } } return 1; } # this checks whether we can upgrade/downgrade between premium & paid sub allow_account_conversion { my ( $class, $u, $to ) = @_; # no existing user; assume no previous conflicting account return 1 unless LJ::isu($u); # no previous paid status; assume no conflicts my $paid_status = DW::Pay::get_paid_status($u); return 1 unless $paid_status; my $from = DW::Pay::type_shortname( $paid_status->{typeid} ); # ok unless we're going from premium to paid or paid to premium my $changing = 0; $changing = 1 if $from eq 'premium' && $to eq 'paid'; $changing = 1 if $from eq 'paid' && $to eq 'premium'; # doesn't match either scenario, so allow it return 1 unless $changing; # allow if we're within two weeks of expiration return 1 if $paid_status->{expiresin} <= 3600 * 24 * 14; # allow upgrading to premium if the remote user owns this account my $remote = LJ::get_remote(); return 1 if $to eq 'premium' && $remote && $remote->can_purchase_for($u); return 0; } # override sub conflicts { my ( $self, $item ) = @_; # if either item are set as "does not conflict" then never say yes return if $self->cannot_conflict || $item->cannot_conflict; # we can only conflict with other items of our own type return if ref $self ne ref $item; # first see if we're talking about the same target # note that we're not checking email here because they may want to buy # multiple paid accounts and send them to all to the same email address # (so they can can create multiple new paid accounts) return if ( $self->t_userid && ( $self->t_userid != $item->t_userid ) ) || ( $self->t_email ); # target same, if both are permanent, then fail because # THERE CAN BE ONLY ONE return LJ::Lang::ml('shop.item.account.conflicts.multipleperms') if $self->permanent && $item->permanent; # otherwise ensure that the classes are the same return LJ::Lang::ml('shop.item.account.conflicts.differentpaid') if $self->class ne $item->class; # guess we allow it return undef; } # override sub t_html { my ( $self, $opts ) = @_; if ( $self->anonymous_target ) { my $random_user_string = LJ::Lang::ml('shop.item.account.randomuser'); if ( $opts->{admin} ) { my $u = LJ::load_userid( $self->t_userid ); return "invalid userid " . $self->t_userid . "" unless $u; return "$random_user_string (" . $u->ljuser_display . ")"; } else { return "$random_user_string"; } } # otherwise, fall back upon default display return $self->SUPER::t_html($opts); } # override sub name_text { my $self = $_[0]; my $name = $self->class_name; if ( $self->cost_points > 0 ) { return LJ::Lang::ml( 'shop.item.account.name.perm', { name => $name, points => $self->cost_points } ) if $self->permanent; return LJ::Lang::ml( 'shop.item.account.name', { name => $name, num => $self->months, points => $self->cost_points } ); } else { return $name if $self->permanent; return LJ::Lang::ml( 'shop.item.account.name.nopoints', { name => $name, num => $self->months } ); } } =head2 C<< $self->class_name >> Return the display name of this account class. =cut sub class_name { my $self = $_[0]; foreach my $cap ( keys %LJ::CAP ) { # skip "unused" elements in CAP next unless $LJ::CAP{$cap}->{_account_type}; return $LJ::CAP{$cap}->{_visible_name} if $LJ::CAP{$cap}->{_account_type} eq $self->class; } return 'Invalid Account Class'; } # simple accessors =head2 C<< $self->months >> Number of months of paid time to be applied. =head2 C<< $self->class >> Account class identifier; not for display. =head2 C<< $self->permanent >> Returns whether this item is for a permanent account, or just a normal paid. =head2 C<< $self->random >> Returns whether this item is for a random user. =head2 C<< $self->anonymous_target >> Returns whether this item for a random user should go to an anonymous user (true) or to an identified user (false) =cut sub months { return $_[0]->{months}; } sub class { return $_[0]->{class}; } sub permanent { return $_[0]->months == 99; } sub random { return $_[0]->{random}; } sub anonymous_target { return $_[0]->{anonymous_target}; } 1;