#!/usr/bin/perl
#
# DW::Shop
#
# General helper class that defines a shopping session and generally facilitate
# a user interacting with stuff.
#
# Authors:
#      Mark Smith <mark@dreamwidth.org>
#
# 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;

use strict;
use Carp qw/ croak confess /;

use DW::Shop::Cart;
use DW::Shop::Engine;

use LJ::ModuleLoader;
LJ::ModuleLoader->require_subclasses("DW::Shop::Item");

# constants across the site
our $MIN_ORDER_COST = 3.00;    # cost in USD minimum.  this only comes into affect if
                               # a user is trying to check out an order that costs
                               # less than this.

# variables we maintain
our $STATE_OPEN        = 1;    # open carts - user can still modify
our $STATE_CHECKOUT    = 2;    # carts have gone through checkout (COMPLETED checkout)
our $STATE_PEND_PAID   = 3;    # waiting on payment confirmation (eCheck?)
our $STATE_PAID        = 4;    # payment received but cart hasn't been processed
our $STATE_PROCESSED   = 5;    # we have received payment for this cart
our $STATE_PEND_REFUND = 6;    # refund is approved but unissued
our $STATE_REFUNDED    = 7;    # we have refunded this cart and reversed it
our $STATE_CLOSED      = 8;    # carts can go from OPEN -> CLOSED
our $STATE_DECLINED    = 9;    # payment entity declined the fundage

# state names, just for helping
our %STATE_NAMES = (
    1 => 'open',
    2 => 'checkout',
    3 => 'pend_paid',
    4 => 'paid',
    5 => 'processed',
    6 => 'pend_refund',
    7 => 'refunded',
    8 => 'closed',
    9 => 'declined'
);

# documentation of valid state transitions...
#
#   OPEN -> CHECKOUT     user has decided to purchase this and we have sent the
#                        payment information to PayPal or Google, but we haven't
#                        heard back on what's going on
#
#   CHECKOUT -> PEND_PAID  PP/GC tells us that the user is paying through some
#                          method that won't let us get the money yet, so we will
#                          have to hold until we hear back again
#
#   PEND_PAID -> PAID    both of these transitions indicate that the user has
#   CHECKOUT -> PAID     really given us the money.  i.e., we've got cash in hand
#                        and we are ready to actually process the cart.
#
#   PAID -> PROCESSED    after we have processed the cart (i.e., granted the paid
#                        time, given the points, etc.)  this lets us know that the
#                        cart is now 'done'.
#
#   PROCESSED -> PEND_REFUND  the user wants a refund and the refund has been
#                             approved.  this is basically a reverse-process step.
#
#   PEND_REFUND -> REFUNDED   once the processing has been complete and we have
#                             unapplied everything that we can, we set state.
#
#   OPEN -> CLOSED       this state is only used if the user has timed out a
#                        cart.  i.e., it hasn't been touched in a while so we
#                        decide the user isn't coming back.
#
#   PEND_PAID -> DECLINED  happens when we try to capture funds from a remote
#                          entity and they decline for some reason.
#
# any other state transition is hereby considered null and void.

# keys are the names of the various payment methods as passed by the cart widget drop-down
# values are hashrefs with id (the integer value that is stored in the 'paymentmethod'
# field in the db) and class (the name of the DW::Shop::Engine class)
our %PAYMENTMETHODS = (
    paypal => {
        id    => 1,
        class => 'PayPal',
    },
    checkmoneyorder => {
        id    => 2,
        class => 'CheckMoneyOrder',
    },
    creditcardpp => {
        id    => 3,
        class => 'CreditCardPP',
    },
    gco => {
        id    => 4,
        class => 'GoogleCheckout',
    },
    creditcard => {
        id    => 5,
        class => 'CreditCard',
    },
    stripe => {
        id    => 6,
        class => 'Stripe',
    },
);

# called to return an instance of the shop; auto-determines if we have a
# remote user and uses that, else, just returns an anonymous shop
sub get {
    my ($class) = @_;

    # easy mode: if we have a remote then we can just toss this into the
    # bucket and have it be used; this trick works because get_remote and
    # such always return the same actual hash within a request
    if ( my $u = LJ::get_remote() ) {
        return $u->{_shop} ||= bless { userid => $u->id }, $class;
    }

    # no remote, so let's note that
    return bless { anon => 1 }, $class;
}

# returns an active cart, if the user has one
sub cart {
    my ($self) = @_;

    return DW::Shop::Cart->get($self);
}

# builds a new cart for the user (throws away existing active)
sub new_cart {
    my ($self) = @_;

    return DW::Shop::Cart->new_cart($self);
}

# gets a link to the active user; this is done this way with a load_userid call
# to prevent circular references.  (we could just make it a weak reference...?)
# FIXME: explore if LJ uses weak references anywhere and if so we can use them
# to store a weakened-$u in $self in initialize()
sub u {
    return undef if $_[0]->{anon} || !$_[0]->{userid};
    return LJ::load_userid( $_[0]->{userid} );
}

# true if this is an anonymous shopping session
sub anonymous {
    return $_[0]->{anon} ? 1 : 0;
}

# returns a text error string if the remote is not allowed to use the
# shop/payment system, undef means they're allowed
sub remote_sysban_check {

    my $err = sub {
        return LJ::Lang::ml(
            'error.blocked',
            {
                blocktype => $_[0],
                email     => $LJ::ACCOUNTS_EMAIL
            }
        );
    };

    # do sysban checks:
    if ( LJ::sysban_check( 'pay_uniq', LJ::UniqCookie->current_uniq ) ) {
        return $err->("computer");
    }
    elsif ( my $remote = LJ::get_remote() ) {
        if ( LJ::sysban_check( 'pay_user', $remote->user ) ) {
            return $err->("account");
        }
        elsif ( LJ::sysban_check( 'pay_email', $remote->email_raw ) ) {
            return $err->("email address");
        }
    }

    # looks good
    return undef;
}

################################################################################
## LJ::User methods
################################################################################

package LJ::User;

use Carp qw/ confess /;

# returns the shop on a user
sub shop {
    my $shop = $_[0]->{_shop}
        or confess 'tried to get shop without calling DW::Shop->initialize()';
    return $shop;
}

1;
