#!/usr/bin/perl # # DW::VirtualGiftTransaction - Support virtual gift transactions # # Authors: # Jen Griffin # # Copyright (c) 2012-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::VirtualGiftTransaction; use strict; use warnings; use DW::Shop::Cart; use DW::VirtualGift; # Because events use this module, Perl warns about redefined subroutines. { no warnings 'redefine'; use LJ::Event::VgiftDelivered; } # IMPLEMENTATION: a blessed hashref with some or all of the following keys: # # From database: transid, rcptid, vgiftid, buyerid, cartid, delivery_t, # accepted, delivered, expired # # From associated shop cart: anon, from, reason, from_text # # For convenience: id, user, vgift, buyer, timestamp (more useful forms) # # Uniqueness of a transaction is determined from rcptid + transid. # # # USAGE: # # DW::VirtualGiftTransaction->load( user => u/uid, id => transid ); # -- loads an existing transaction, returns object # # DW::VirtualGiftTransaction->save( user => u/uid, vgift => vgiftid ); # -- saves a new transaction, returns transaction ID # # DW::VirtualGiftTransaction->list( user => u/uid, profile => 1/0 ); # -- returns the list of transaction objects for the given user # # # Methods for transaction objects: # # Properties: id, u, url # Queries: is_delivered, is_accepted, is_expired, is_anonymous # Actions: remove, expire, accept, deliver, notify_delivered # Display: view, from_html, from_text sub save { my ( $class, %opts ) = @_; # opts: user => (u or userid) - mandatory # vgift => (obj or id) - mandatory # cartid => (cartid) - optional # buyer => (u or userid) - optional # time => (epoch seconds) - optional (defaults to current time) my $vg = $opts{vgift} or return; my $vgift = ref $vg ? $vg : DW::VirtualGift->new($vg); return unless $vgift && $vgift->id; my $u = LJ::want_user( $opts{user} ) or return; my $id = LJ::alloc_user_counter( $u, 'V' ) or return; my $secs = $opts{time} || time; my $buyer = LJ::want_user( $opts{buyer} ); my $buyerid = $buyer ? $buyer->id : 0; $u->do( 'INSERT INTO vgift_trans (transid, rcptid, vgiftid, buyerid,' . ' cartid, delivery_t) VALUES (?, ?, ?, ?, ?, ?)', undef, $id, $u->id, $vgift->id, $buyerid, $opts{cartid}, $secs ); die $u->errstr if $u->err; # update the vgift_counts table $vgift->mark_sold; # memcache expiration for list of all transactions LJ::MemCache::delete( $class->_transaction_list_memkey($u) ); return $id; # not object } sub list { my ( $class, %opts ) = @_; my $u = LJ::want_user( $opts{user} ) or return; my $memkey = $class->_transaction_list_memkey($u); my $data = LJ::MemCache::get($memkey); unless ( defined $data ) { # Note: we pretend undelivered gifts don't exist yet. $data = $u->selectcol_arrayref( "SELECT transid FROM vgift_trans" . " WHERE rcptid=? AND delivered='Y' ORDER BY delivery_t DESC, " . " transid DESC", undef, $u->id ) || []; die $u->errstr if $u->err; LJ::MemCache::set( $memkey, $data ); } # transform transaction IDs to objects my @loaded = grep { defined } map { $class->load( user => $u, id => $_ ) } @$data; # do any further filtering of results in caller return @loaded unless $opts{profile}; # special case: profile only shows accepted & non-expired return grep { $_->is_accepted && !$_->is_expired } @loaded; } sub load { my ( $class, %opts ) = @_; # opts: user => (u or userid) - mandatory # id => (transaction id) - mandatory return unless defined $opts{id}; my $id = $opts{id} + 0; my $u = LJ::want_user( $opts{user} ) or return; my $memkey = $class->_transaction_load_memkey( $u, $id ); my $data = LJ::MemCache::get($memkey); unless ( defined $data ) { $data = $u->selectrow_hashref( 'SELECT transid, rcptid, vgiftid,' . ' buyerid, cartid, delivery_t, accepted, delivered, expired' . ' FROM vgift_trans WHERE rcptid=? AND transid=?', undef, $u->id, $id ) || {}; die $u->errstr if $u->err; if ( my $item = $class->_search_cart($data) ) { $data->{reason} = $item->reason; $data->{anon} = $item->anonymous; # from_html takes care of the anon/email/username display # if the item is found. otherwise fall back to using buyerid. $data->{from} = $item->from_html ne LJ::Lang::ml('error.nojournal') ? $item->from_html : undef; $data->{from_text} = $item->from_text ne LJ::Lang::ml('error.nojournal') ? $item->from_text : undef; } LJ::MemCache::set( $memkey, $data ); } return {} unless %$data; # populate some extra hash keys for convenience $data->{id} = $data->{transid}; $data->{user} = LJ::want_user( $data->{rcptid} ); $data->{vgift} = DW::VirtualGift->new( $data->{vgiftid} ); $data->{buyer} = LJ::want_user( $data->{buyerid} ); $data->{timestamp} = LJ::mysql_time( $data->{delivery_t} ); return $class->new($data); } sub _search_cart { my ( $class, $data ) = @_; my $cart = DW::Shop::Cart->get_from_cartid( $data->{cartid} ) or return; foreach my $item ( @{ $cart->items } ) { next unless ref $item eq 'DW::Shop::Item::VirtualGift'; next unless $data->{rcptid} == $item->t_userid; next unless $data->{transid} == $item->vgift_transid; # if we get here, it's the right item return $item; } # we didn't find it - sadness return undef; } sub _transaction_load_memkey { my ( $class, $u, $id ) = @_; my $uid = $u->id or return; return [ $uid, "vgift.trans.$id" ]; # caches database row } sub _transaction_list_memkey { my ( $class, $u ) = @_; my $uid = $u->id or return; return [ $uid, "vgift.translist.$uid" ]; # caches list of transids } sub new { my ( $class, $self ) = @_; $class = ref $class if ref $class; return $self if ref $self eq $class; # already blessed my ( $id, $uid ) = ( $self->{transid}, $self->{rcptid} ); return unless ( $id && $id =~ /^\d+$/ ) && ( $uid && $uid =~ /^\d+$/ ); bless $self, $class; return $self; } ### OBJECT METHODS ### sub id { $_[0]->{id} } sub u { $_[0]->{user} } sub is_delivered { $_[0]->{delivered} eq 'Y' } sub is_accepted { $_[0]->{accepted} eq 'Y' } sub is_expired { $_[0]->{expired} eq 'Y' } sub is_anonymous { $_[0]->{anon} ? 1 : 0 } sub from_html { my ($self) = @_; return $self->{from} if defined $self->{from}; return $self->{buyer}->ljuser_display if LJ::isu( $self->{buyer} ); # undefined if neither of these is valid } sub from_text { my ($self) = @_; return $self->{from_text} if defined $self->{from_text}; return $self->{buyer}->display_name if LJ::isu( $self->{buyer} ); # undefined if neither of these is valid } sub _update { my ( $self, $sql, $expire ) = @_; my ( $id, $u ) = ( $self->id, $self->u ); return unless $id && LJ::isu($u); return unless $sql && $sql !~ /\?/; $u->do( "$sql WHERE rcptid=? AND transid=?", undef, $u->id, $id ); die $u->errstr if $u->err; # memcache expiration for this one transaction LJ::MemCache::delete( $self->_transaction_load_memkey( $u, $id ) ); # memcache expiration for list of all transactions # only needed for deliveries and deletions LJ::MemCache::delete( $self->_transaction_list_memkey($u) ) if $expire; return 1; } sub remove { my ($self) = @_; return $self->_update( 'DELETE FROM vgift_trans', 1 ); } sub accept { my ($self) = @_; return 1 if $self->is_accepted; # already accepted $self->{accepted} = 'Y'; # update object in memory return $self->_update('UPDATE vgift_trans SET accepted="Y"'); } sub deliver { my ($self) = @_; return 1 if $self->is_delivered; # already delivered $self->{delivered} = 'Y'; # update object in memory return $self->_update( 'UPDATE vgift_trans SET delivered="Y", ' . 'delivery_t=UNIX_TIMESTAMP()', 1 ); } sub notify_delivered { my ($self) = @_; # make sure the gift was actually delivered return unless $self->is_delivered; # notify the user (no opt-out) my @args = ( $self->u, $self->id ); LJ::Event::VgiftDelivered->new(@args)->fire; } sub url { my ($self) = @_; return unless LJ::isu( $self->u ); return $self->u->journal_base . "/vgifts/" . $self->id; } sub view { # print mini view for profile page; standalone page should be TT # (expects virtualgift class in htdocs/stc/profile.css) my ($self) = @_; my $vg = $self->{vgift}; return '' unless $vg && $vg->id; my $disp = $vg->img_small_html; # substitute the gift name if no image is set $disp = $vg->name_ehtml unless $disp =~ /^url; $disp = "$disp" if $url; my $ret = "
$disp
"; my $from_word = LJ::Lang::ml('widget.shopcart.header.from'); my $anon_word = LJ::Lang::ml('widget.shopcart.anonymous'); if ( $self->{anon} ) { $ret .= $from_word . " " . $anon_word; } elsif ( $self->{from} ) { # show the cached result of the cart item's from_html method $ret .= $from_word . " " . $self->{from}; } elsif ( LJ::isu( $self->{buyer} ) ) { $ret .= $from_word . " " . $self->{buyer}->ljuser_display; } else { # if we can't show a user name, just print anonymous $ret .= $from_word . " " . $anon_word; } $ret .= "
\n"; return $ret; } 1;