638 lines
18 KiB
Perl
638 lines
18 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# DW::Shop::Cart
|
||
|
|
#
|
||
|
|
# Encapsulates a shopping cart for a user. Handles loading, saving, modifying
|
||
|
|
# and all other actions of a shopping cart.
|
||
|
|
#
|
||
|
|
# Authors:
|
||
|
|
# Mark Smith <mark@dreamwidth.org>
|
||
|
|
# Janine Smith <janine@netrophic.com>
|
||
|
|
#
|
||
|
|
# Copyright (c) 2009-2010 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::Cart;
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
use Carp qw/ croak confess /;
|
||
|
|
use Storable qw/ nfreeze thaw /;
|
||
|
|
|
||
|
|
use DW::Shop;
|
||
|
|
|
||
|
|
# returns a created cart for a given shop
|
||
|
|
sub get {
|
||
|
|
my ( $class, $shop ) = @_;
|
||
|
|
|
||
|
|
# see if the shop has a user or if it's anonymous
|
||
|
|
my ( $u, $sql, @bind );
|
||
|
|
if ( $shop->anonymous ) {
|
||
|
|
|
||
|
|
# if they don't have a unique cookie and they're anonymous, we aren't
|
||
|
|
# presently equipped to let them shop
|
||
|
|
my $uniq = LJ::UniqCookie->current_uniq
|
||
|
|
or return undef;
|
||
|
|
|
||
|
|
$sql = 'uniq = ? AND userid IS NULL';
|
||
|
|
@bind = ($uniq);
|
||
|
|
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$u = $shop->u
|
||
|
|
or confess 'shop has no user object';
|
||
|
|
|
||
|
|
# return this cart if loaded already
|
||
|
|
return $u->{_cart} if $u->{_cart};
|
||
|
|
|
||
|
|
# faaail, have to load it
|
||
|
|
$sql = 'userid = ?';
|
||
|
|
@bind = ( $u->id );
|
||
|
|
}
|
||
|
|
|
||
|
|
# see if they had one in the database
|
||
|
|
my $dbh = LJ::get_db_writer()
|
||
|
|
or return undef;
|
||
|
|
my $dbcart = $dbh->selectrow_hashref(
|
||
|
|
qq{SELECT cartblob
|
||
|
|
FROM shop_carts
|
||
|
|
WHERE $sql AND state = ?
|
||
|
|
ORDER BY starttime DESC
|
||
|
|
LIMIT 1},
|
||
|
|
undef, @bind, $DW::Shop::STATE_OPEN
|
||
|
|
);
|
||
|
|
|
||
|
|
# if we got something, thaw the blob and return
|
||
|
|
if ($dbcart) {
|
||
|
|
my $cart = $class->_build( thaw( $dbcart->{cartblob} ) );
|
||
|
|
if ($u) {
|
||
|
|
$u->{_cart} = $cart;
|
||
|
|
}
|
||
|
|
return $cart;
|
||
|
|
}
|
||
|
|
|
||
|
|
# no existing cart, so build a new one \o/
|
||
|
|
return $class->new_cart($u);
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns a new cart given a cartid
|
||
|
|
sub get_from_cartid {
|
||
|
|
my ( $class, $cartid ) = @_;
|
||
|
|
return undef
|
||
|
|
unless defined $cartid && $cartid > 0;
|
||
|
|
|
||
|
|
# see if they had one in the database
|
||
|
|
my $dbh = LJ::get_db_writer()
|
||
|
|
or return undef;
|
||
|
|
my $dbcart = $dbh->selectrow_hashref(
|
||
|
|
qq{SELECT cartblob
|
||
|
|
FROM shop_carts WHERE cartid = ?},
|
||
|
|
undef, $cartid
|
||
|
|
);
|
||
|
|
return undef unless $dbcart;
|
||
|
|
|
||
|
|
# if we got something, thaw the blob and return
|
||
|
|
return $class->_build( thaw( $dbcart->{cartblob} ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns a new cart given an ordernum
|
||
|
|
sub get_from_ordernum {
|
||
|
|
my ( $class, $ordernum ) = @_;
|
||
|
|
my ( $cartid, $authcode );
|
||
|
|
|
||
|
|
( $cartid, $authcode ) = ( $1 + 0, $2 )
|
||
|
|
if $ordernum =~ /^(\d+)-(.+)$/;
|
||
|
|
return undef
|
||
|
|
unless $cartid && $cartid > 0;
|
||
|
|
return undef
|
||
|
|
unless $authcode && length($authcode) == 20;
|
||
|
|
|
||
|
|
# see if they had one in the database
|
||
|
|
my $cart = $class->get_from_cartid($cartid);
|
||
|
|
return undef
|
||
|
|
unless $cart && $cart->authcode eq $authcode;
|
||
|
|
|
||
|
|
# all matches, so return this cart
|
||
|
|
return $cart;
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns a new cart given an invite code
|
||
|
|
# if scalar ref 'itemidref' is passed, store the itemid for the invite code in it
|
||
|
|
sub get_from_invite {
|
||
|
|
my ( $class, $code, %opts ) = @_;
|
||
|
|
|
||
|
|
my $itemidref = $opts{itemidref};
|
||
|
|
|
||
|
|
my ($acid) = DW::InviteCodes->decode($code);
|
||
|
|
return undef
|
||
|
|
unless defined $acid && $acid > 0;
|
||
|
|
|
||
|
|
my $dbh = LJ::get_db_writer()
|
||
|
|
or return undef;
|
||
|
|
my $dbret = $dbh->selectrow_hashref(
|
||
|
|
qq{SELECT cartid, itemid
|
||
|
|
FROM shop_codes WHERE acid = ?},
|
||
|
|
undef, $acid
|
||
|
|
);
|
||
|
|
return undef unless $dbret;
|
||
|
|
|
||
|
|
$$itemidref = $dbret->{itemid} if ref $itemidref eq 'SCALAR';
|
||
|
|
return $class->get_from_cartid( $dbret->{cartid} );
|
||
|
|
}
|
||
|
|
|
||
|
|
# creating a new cart implicitly activates. just so you know. this function
|
||
|
|
# will build a new empty cart for the user. but user is optional and we will
|
||
|
|
# build a cart for the current uniq.
|
||
|
|
sub new_cart {
|
||
|
|
my ( $class, $u ) = @_;
|
||
|
|
$u = LJ::want_user($u);
|
||
|
|
|
||
|
|
my $cartid = LJ::alloc_global_counter('H')
|
||
|
|
or return undef;
|
||
|
|
|
||
|
|
# this is a blank cart containing no items
|
||
|
|
my $cart = {
|
||
|
|
cartid => $cartid,
|
||
|
|
starttime => time(),
|
||
|
|
userid => $u ? $u->id : undef,
|
||
|
|
ip => LJ::get_remote_ip(),
|
||
|
|
state => $DW::Shop::STATE_OPEN,
|
||
|
|
items => [],
|
||
|
|
total_cash => 0.00,
|
||
|
|
total_points => 0,
|
||
|
|
nextscan => 0,
|
||
|
|
authcode => LJ::make_auth_code(20),
|
||
|
|
paymentmethod => 0, # we don't have a payment method yet
|
||
|
|
pm_metadata => {}, # payment method extra storage
|
||
|
|
email => undef, # we don't have an email yet
|
||
|
|
};
|
||
|
|
|
||
|
|
# if uniq undef, hash definition is totally wrecked, so set this separately
|
||
|
|
$cart->{uniq} = LJ::UniqCookie->current_uniq;
|
||
|
|
|
||
|
|
# now, delete any old carts we don't need
|
||
|
|
my $dbh = LJ::get_db_writer()
|
||
|
|
or return undef;
|
||
|
|
if ( defined $cart->{userid} ) {
|
||
|
|
$dbh->do( q{UPDATE shop_carts SET state = ? WHERE userid = ? AND state = ?},
|
||
|
|
undef, $DW::Shop::STATE_CLOSED, $cart->{userid}, $DW::Shop::STATE_OPEN );
|
||
|
|
croak $dbh->errstr if $dbh->err;
|
||
|
|
}
|
||
|
|
if ( defined $cart->{uniq} ) {
|
||
|
|
$dbh->do( q{UPDATE shop_carts SET state = ? WHERE uniq = ? AND state = ?},
|
||
|
|
undef, $DW::Shop::STATE_CLOSED, $cart->{uniq}, $DW::Shop::STATE_OPEN );
|
||
|
|
croak $dbh->errstr if $dbh->err;
|
||
|
|
}
|
||
|
|
|
||
|
|
# build this into an object and activate it
|
||
|
|
$cart = $class->_build($cart);
|
||
|
|
|
||
|
|
# now persist the cart
|
||
|
|
$cart->save;
|
||
|
|
$u->{_cart} = $cart if $u;
|
||
|
|
|
||
|
|
DW::Stats::increment( 'dw.shop.cart.new', 1, [ 'anonymous:' . ( $u ? 'no' : 'yes' ) ] );
|
||
|
|
|
||
|
|
# we're done
|
||
|
|
return $cart;
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns all carts that the given user has ever had
|
||
|
|
# can pass 'finished' opt which will omit carts in the OPEN, CLOSED, or
|
||
|
|
# CHECKOUT states
|
||
|
|
sub get_all {
|
||
|
|
my ( $class, $u, %opts ) = @_;
|
||
|
|
$u = LJ::want_user($u);
|
||
|
|
|
||
|
|
my $extra_sql =
|
||
|
|
$opts{finished}
|
||
|
|
? " AND state NOT IN ($DW::Shop::STATE_OPEN,"
|
||
|
|
. " $DW::Shop::STATE_CLOSED,"
|
||
|
|
. " $DW::Shop::STATE_CHECKOUT)"
|
||
|
|
: "";
|
||
|
|
|
||
|
|
my $dbh = LJ::get_db_writer()
|
||
|
|
or return undef;
|
||
|
|
my $sth = $dbh->prepare("SELECT cartblob FROM shop_carts WHERE userid = ?$extra_sql");
|
||
|
|
$sth->execute( $u->id );
|
||
|
|
|
||
|
|
my @carts = ();
|
||
|
|
while ( my $cart = $sth->fetchrow_hashref ) {
|
||
|
|
push @carts, $class->_build( thaw( $cart->{cartblob} ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
return @carts;
|
||
|
|
}
|
||
|
|
|
||
|
|
# saves the current cart to the database, returns 1/0
|
||
|
|
sub save {
|
||
|
|
my ( $self, %opts ) = @_;
|
||
|
|
|
||
|
|
# we store the payment method id in the db
|
||
|
|
my $paymentmethod_id = $DW::Shop::PAYMENTMETHODS{ $self->paymentmethod }->{id} || 0;
|
||
|
|
|
||
|
|
# toss in the database
|
||
|
|
my $dbh = LJ::get_db_writer()
|
||
|
|
or return undef;
|
||
|
|
$dbh->do(
|
||
|
|
q{REPLACE INTO shop_carts (userid, cartid, starttime, uniq, state, nextscan, authcode, email, paymentmethod, cartblob)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)},
|
||
|
|
undef,
|
||
|
|
( map { $self->{$_} } qw/ userid cartid starttime uniq state nextscan authcode email / ),
|
||
|
|
$paymentmethod_id, nfreeze($self)
|
||
|
|
);
|
||
|
|
|
||
|
|
# bail if error
|
||
|
|
return 0 if $dbh->err;
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns an engine for this cart
|
||
|
|
sub engine {
|
||
|
|
my $self = $_[0];
|
||
|
|
|
||
|
|
return $self->{_engine} ||= DW::Shop::Engine->get( $self->paymentmethod => $self );
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns the number of items in this cart
|
||
|
|
sub num_items {
|
||
|
|
my $self = $_[0];
|
||
|
|
|
||
|
|
return scalar @{ $self->{items} || [] };
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns 1/0 if this cart has any items in it
|
||
|
|
sub has_items {
|
||
|
|
my $self = $_[0];
|
||
|
|
|
||
|
|
return $self->num_items > 0 ? 1 : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
# add an item to the shopping cart, returns 1/0
|
||
|
|
sub add_item {
|
||
|
|
my ( $self, $item ) = @_;
|
||
|
|
return unless $self && $item;
|
||
|
|
|
||
|
|
die "Attempted to alter cart not in OPEN state.\n"
|
||
|
|
unless $self->state == $DW::Shop::STATE_OPEN;
|
||
|
|
|
||
|
|
# tell the item who we are
|
||
|
|
$item->cartid( $self->id );
|
||
|
|
|
||
|
|
# make sure this item is allowed to be added
|
||
|
|
my $error;
|
||
|
|
unless (
|
||
|
|
$item->can_be_added( errref => \$error, user_confirmed => delete $item->{user_confirmed} ) )
|
||
|
|
{
|
||
|
|
return ( 0, $error );
|
||
|
|
}
|
||
|
|
|
||
|
|
# iterate over existing items to see if any conflict
|
||
|
|
foreach my $it ( @{ $self->items } ) {
|
||
|
|
if ( my $rv = $it->conflicts($item) ) {
|
||
|
|
|
||
|
|
# this return value is so messed up... WTB exceptions
|
||
|
|
return ( 0, $rv );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# construct a new, unique id for this item
|
||
|
|
my $itid = LJ::alloc_global_counter('I')
|
||
|
|
or return ( 0, 'Failed to allocate item counter.' );
|
||
|
|
$item->id($itid);
|
||
|
|
|
||
|
|
# looks good, so let's add it...
|
||
|
|
push @{ $self->items }, $item;
|
||
|
|
$self->recalculate_costs;
|
||
|
|
|
||
|
|
# now call out to the hook system in case anybody wants to munge with us
|
||
|
|
LJ::Hooks::run_hooks( 'shop_cart_added_item', $self, $item );
|
||
|
|
|
||
|
|
# save to db and return
|
||
|
|
$self->_touch;
|
||
|
|
$self->save || return ( 0, 'Unable to save cart.' );
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# removes an item from this cart by id
|
||
|
|
sub remove_item {
|
||
|
|
my ( $self, $id, %opts ) = @_;
|
||
|
|
return unless $self && $id;
|
||
|
|
|
||
|
|
die "Attempted to alter cart not in OPEN state.\n"
|
||
|
|
unless $self->state == $DW::Shop::STATE_OPEN;
|
||
|
|
|
||
|
|
my ( $removed, $out ) = ( undef, [] );
|
||
|
|
foreach my $it ( @{ $self->items } ) {
|
||
|
|
if ( $it->id == $id ) {
|
||
|
|
|
||
|
|
# some items are noremove items
|
||
|
|
if ( $it->noremove && !$opts{force} ) {
|
||
|
|
push @$out, $it;
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
# advise that we removed an item from the cart
|
||
|
|
die "Attempted to remove two items in one pass with id $id.\n"
|
||
|
|
if defined $removed;
|
||
|
|
$removed = $it;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
push @$out, $it;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$self->{items} = $out;
|
||
|
|
|
||
|
|
# now recalculate the costs and save
|
||
|
|
$self->recalculate_costs;
|
||
|
|
$self->_touch;
|
||
|
|
$self->save;
|
||
|
|
|
||
|
|
# now run the hook, this is later so that we've updated the cart already
|
||
|
|
LJ::Hooks::run_hooks( 'shop_cart_removed_item', $self, $removed );
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub recalculate_costs {
|
||
|
|
my $self = $_[0];
|
||
|
|
|
||
|
|
# if we're not in the OPEN state, do not recalculate. the prices are fixed.
|
||
|
|
return unless $self->state == $DW::Shop::STATE_OPEN;
|
||
|
|
|
||
|
|
my ( $has_points, $max_points ) = ( 0, 0 );
|
||
|
|
if ( $self->userid ) {
|
||
|
|
my $u = LJ::load_userid( $self->userid );
|
||
|
|
$has_points = $u->shop_points;
|
||
|
|
}
|
||
|
|
|
||
|
|
# we have to determine the total cost of the order first so we can do the
|
||
|
|
# minimum order size calculations later
|
||
|
|
( $self->{total_points}, $self->{total_cash} ) = ( 0, 0.00 );
|
||
|
|
foreach my $item ( @{ $self->items } ) {
|
||
|
|
$self->{total_cash} += $item->paid_cash( $item->cost_cash );
|
||
|
|
$item->paid_points(0);
|
||
|
|
$max_points += $item->cost_points;
|
||
|
|
}
|
||
|
|
|
||
|
|
# if the user has no points, we're done
|
||
|
|
return unless $has_points;
|
||
|
|
|
||
|
|
# now, if we're short on points, the maximum we can use is based on the
|
||
|
|
# minimum cash order size
|
||
|
|
my $all_points = 0;
|
||
|
|
if ( $has_points < $max_points ) {
|
||
|
|
|
||
|
|
# x10 to convert from USD to points
|
||
|
|
my $cutoff = $max_points - ( $DW::Shop::MIN_ORDER_COST * 10 );
|
||
|
|
|
||
|
|
# now we effectively constrain the ceiling of how many points the user
|
||
|
|
# has to the point that makes the cash equivalent $3.00
|
||
|
|
$has_points = $cutoff
|
||
|
|
if $has_points > $cutoff;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
# user is all poitns
|
||
|
|
$all_points = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# second loop has to iterate and actually adjust the point/cash balances
|
||
|
|
foreach my $item ( @{ $self->items } ) {
|
||
|
|
|
||
|
|
# in some cases, we have items that cost no points, those items
|
||
|
|
# we can just ignore and skip
|
||
|
|
next unless $item->cost_points;
|
||
|
|
|
||
|
|
# start deducting items from points until one goes negative. note that
|
||
|
|
# every item has to cost at least 1 cent, or Stripe will be unhappy.
|
||
|
|
$has_points -= $item->cost_points;
|
||
|
|
|
||
|
|
# if positive, the item was paid for by points entirely, but we need
|
||
|
|
# to respect Stripe's minimum cost rules
|
||
|
|
if ( $has_points >= 0 ) {
|
||
|
|
if ($all_points) {
|
||
|
|
|
||
|
|
# No cash
|
||
|
|
$item->paid_cash(0.0);
|
||
|
|
$item->paid_points( $item->cost_points );
|
||
|
|
|
||
|
|
$self->{total_cash} -= $item->cost_cash;
|
||
|
|
$self->{total_points} += $item->cost_points;
|
||
|
|
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
# Respect Stripe minimum cost
|
||
|
|
$item->paid_cash(0.1);
|
||
|
|
$item->paid_points( $item->cost_points - 1 );
|
||
|
|
$has_points++;
|
||
|
|
|
||
|
|
$self->{total_cash} -= ( $item->cost_cash - 0.1 );
|
||
|
|
$self->{total_points} += ( $item->cost_points - 1 );
|
||
|
|
}
|
||
|
|
|
||
|
|
# and last if we're at 0 points left
|
||
|
|
last if $has_points == 0;
|
||
|
|
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
my $cash = -$has_points;
|
||
|
|
$item->paid_cash( $cash / 10 );
|
||
|
|
$item->paid_points( $item->cost_points - $cash );
|
||
|
|
|
||
|
|
$self->{total_cash} -= $item->cost_cash - $item->paid_cash;
|
||
|
|
$self->{total_points} += $item->paid_points;
|
||
|
|
|
||
|
|
# and this means we're done
|
||
|
|
last;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# given an itemid that's in this cart, return it
|
||
|
|
sub get_item {
|
||
|
|
my ( $self, $id ) = @_;
|
||
|
|
|
||
|
|
foreach my $it ( @{ $self->items } ) {
|
||
|
|
return $it if $it->id == $id;
|
||
|
|
}
|
||
|
|
|
||
|
|
return undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
# get/set state
|
||
|
|
sub state {
|
||
|
|
my ( $self, $newstate ) = @_;
|
||
|
|
return $self->{state} unless defined $newstate;
|
||
|
|
return $self->{state} if $self->{state} == $newstate;
|
||
|
|
|
||
|
|
# alert the items that the cart's state has changed, this allows items to do things
|
||
|
|
# that happen when the state changes.
|
||
|
|
$_->cart_state_changed($newstate) foreach @{ $self->items };
|
||
|
|
|
||
|
|
LJ::Hooks::run_hooks( 'shop_cart_state_change', $self, $newstate );
|
||
|
|
DW::Stats::increment(
|
||
|
|
'dw.shop.cart.state_change',
|
||
|
|
1,
|
||
|
|
[
|
||
|
|
'from_state:' . $DW::Shop::STATE_NAMES{ $self->{state} },
|
||
|
|
'to_state:' . $DW::Shop::STATE_NAMES{$newstate}
|
||
|
|
]
|
||
|
|
);
|
||
|
|
|
||
|
|
$self->_notify_buyer_paid if $newstate == $DW::Shop::STATE_PROCESSED;
|
||
|
|
|
||
|
|
$self->{state} = $newstate;
|
||
|
|
$self->save;
|
||
|
|
|
||
|
|
return $self->{state};
|
||
|
|
}
|
||
|
|
|
||
|
|
# get/set payment method
|
||
|
|
sub paymentmethod {
|
||
|
|
my ( $self, $newpaymentmethod ) = @_;
|
||
|
|
|
||
|
|
return $self->{paymentmethod}
|
||
|
|
unless defined $newpaymentmethod;
|
||
|
|
|
||
|
|
$self->{paymentmethod} = $newpaymentmethod;
|
||
|
|
$self->save;
|
||
|
|
|
||
|
|
return $self->{paymentmethod};
|
||
|
|
}
|
||
|
|
|
||
|
|
# return hash for paymenet method metadata
|
||
|
|
sub paymentmethod_metadata {
|
||
|
|
my ( $self, $key, $val ) = @_;
|
||
|
|
|
||
|
|
if ( defined $val ) {
|
||
|
|
$self->{pm_metadata}->{$key} = $val;
|
||
|
|
$self->save;
|
||
|
|
}
|
||
|
|
return $self->{pm_metadata}->{$key};
|
||
|
|
}
|
||
|
|
|
||
|
|
# payment method the user should be aware of
|
||
|
|
sub paymentmethod_visible {
|
||
|
|
my $self = $_[0];
|
||
|
|
|
||
|
|
my $paymentmethod = $self->{paymentmethod};
|
||
|
|
return $paymentmethod unless $paymentmethod eq "checkmoneyorder";
|
||
|
|
return ( $self->total_cash == 0 ) ? "points" : $paymentmethod;
|
||
|
|
}
|
||
|
|
|
||
|
|
# get/set email address
|
||
|
|
sub email {
|
||
|
|
my ( $self, $newemail ) = @_;
|
||
|
|
|
||
|
|
return $self->{email}
|
||
|
|
unless defined $newemail;
|
||
|
|
|
||
|
|
$self->{email} = $newemail;
|
||
|
|
$self->save;
|
||
|
|
|
||
|
|
return $self->{email};
|
||
|
|
}
|
||
|
|
|
||
|
|
################################################################################
|
||
|
|
## read-only accessor methods
|
||
|
|
################################################################################
|
||
|
|
|
||
|
|
sub id { $_[0]->{cartid} }
|
||
|
|
sub userid { $_[0]->{userid} }
|
||
|
|
sub starttime { $_[0]->{starttime} }
|
||
|
|
sub age { time() - $_[0]->{starttime} }
|
||
|
|
sub items { $_[0]->{items} ||= [] }
|
||
|
|
sub ip { $_[0]->{ip} }
|
||
|
|
sub uniq { $_[0]->{uniq} }
|
||
|
|
sub nextscan { $_[0]->{nextscan} }
|
||
|
|
sub authcode { $_[0]->{authcode} }
|
||
|
|
sub total_points { $_[0]->{total_points} + 0 }
|
||
|
|
sub ordernum { $_[0]->{cartid} . '-' . $_[0]->{authcode} }
|
||
|
|
|
||
|
|
# this has to work for both old items (pre-points) and new ones
|
||
|
|
sub total_cash {
|
||
|
|
my $self = $_[0];
|
||
|
|
return $self->{total} + 0.00 if exists $self->{total};
|
||
|
|
return $self->{total_cash} + 0.00;
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns the total in a displayed format
|
||
|
|
sub display_total {
|
||
|
|
my $self = $_[0];
|
||
|
|
if ( $self->total_cash && $self->total_points ) {
|
||
|
|
return sprintf( '$%0.2f USD and %d points', $self->total_cash, $self->total_points );
|
||
|
|
}
|
||
|
|
elsif ( $self->total_cash ) {
|
||
|
|
return sprintf( '$%0.2f USD', $self->total_cash );
|
||
|
|
}
|
||
|
|
elsif ( $self->total_points ) {
|
||
|
|
return sprintf( '%d points', $self->total_points );
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
return 'free';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
sub display_total_cash { sprintf( '$%0.2f USD', $_[0]->total_cash ) }
|
||
|
|
sub display_total_points { sprintf( '%d points', $_[0]->total_points ) }
|
||
|
|
|
||
|
|
################################################################################
|
||
|
|
## internal cart methods
|
||
|
|
################################################################################
|
||
|
|
|
||
|
|
# turns a hashref cart into a cart object
|
||
|
|
sub _build {
|
||
|
|
my ( $class, $cart ) = @_;
|
||
|
|
ref $cart eq 'HASH' or return $cart;
|
||
|
|
|
||
|
|
# simply blesses ... although in the future we might do some sanity checking
|
||
|
|
# here to make sure we have good data, if that proves to be necessary.
|
||
|
|
return bless $cart, $class;
|
||
|
|
}
|
||
|
|
|
||
|
|
# called to update our access time, this is mostly an internal method, but anybody
|
||
|
|
# that has reason to can call it; note that this needs to be called before a save
|
||
|
|
sub _touch {
|
||
|
|
$_[0]->{starttime} = time;
|
||
|
|
}
|
||
|
|
|
||
|
|
# let the cart owner know that their purchase has just gone through.
|
||
|
|
sub _notify_buyer_paid {
|
||
|
|
my $self = $_[0];
|
||
|
|
|
||
|
|
my $u = LJ::load_userid( $self->{userid} );
|
||
|
|
|
||
|
|
my @payment_methods;
|
||
|
|
push @payment_methods, '$' . $self->total_cash . ' USD'
|
||
|
|
if $self->total_cash;
|
||
|
|
push @payment_methods, $self->total_points . ' points'
|
||
|
|
if $self->total_points;
|
||
|
|
|
||
|
|
my $itemlist = join( "\n", map { " * " . $_->short_desc( nohtml => 1 ) } @{ $self->items } );
|
||
|
|
|
||
|
|
LJ::send_mail(
|
||
|
|
{
|
||
|
|
to => $self->email,
|
||
|
|
from => $LJ::ACCOUNTS_EMAIL,
|
||
|
|
fromname => $LJ::SITENAME,
|
||
|
|
subject =>
|
||
|
|
LJ::Lang::ml( "shop.email.processed.subject", { sitename => $LJ::SITENAME } ),
|
||
|
|
body => LJ::Lang::ml(
|
||
|
|
"shop.email.processed.body",
|
||
|
|
{
|
||
|
|
touser => LJ::isu($u) ? $u->display_name : $self->email,
|
||
|
|
price => join( ", ", @payment_methods ),
|
||
|
|
itemlist => $itemlist,
|
||
|
|
receipturl => "$LJ::SHOPROOT/receipt?ordernum=" . $self->ordernum,
|
||
|
|
sitename => $LJ::SITENAME,
|
||
|
|
}
|
||
|
|
),
|
||
|
|
}
|
||
|
|
) unless $LJ::T_SUPPRESS_EMAIL;
|
||
|
|
}
|
||
|
|
|
||
|
|
1;
|