webgui/lib/WebGUI/Shop/Cart.pm
2011-10-31 20:13:01 -07:00

1362 lines
44 KiB
Perl

package WebGUI::Shop::Cart;
use strict;
use Scalar::Util qw/blessed/;
use Moose;
use WebGUI::Definition;
property 'shippingAddressId' => (
noFormPost => 1,
default => '',
);
property 'billingAddressId' => (
noFormPost => 1,
default => '',
);
property 'shipperId' => (
noFormPost => 1,
default => '',
);
property 'gatewayId' => (
noFormPost => 1,
default => '',
);
property 'posUserId' => (
noFormPost => 1,
default => '',
);
property creationDate => (
required => 1,
noFormPost => 1,
default => '',
);
has [ qw/cartId session/] => (
is => 'ro',
required => 1,
);
has sessionId => (
is => 'ro',
lazy => 1,
builder => '_default_sessionId',
);
sub _default_sessionId {
my $self = shift;
return $self->session->getId;
}
has error => (
is => 'rw',
);
use JSON;
use WebGUI::Asset::Template;
use WebGUI::Exception::Shop;
use WebGUI::Form;
use WebGUI::International;
use WebGUI::Shop::AddressBook;
use WebGUI::Shop::CartItem;
use WebGUI::Shop::Credit;
use WebGUI::Shop::Ship;
use WebGUI::Shop::Pay;
use WebGUI::Shop::Tax;
use WebGUI::User;
use Tie::IxHash;
use Data::Dumper;
=head1 NAME
Package WebGUI::Shop::Cart
=head1 DESCRIPTION
The cart is the glue that holds a user's order together until they're ready to check out.
=head1 SYNOPSIS
use WebGUI::Shop::Cart;
my $cart = WebGUI::Shop::Cart->new($session);
=head1 METHODS
These subroutines are available from this package:
=cut
#-------------------------------------------------------------------
=head2 new ( $session, $cartId )
Constructor. Instanciates a cart based upon a cartId.
=head2 new ( $session )
Constructor. Builds a new, default cart object.
=head2 new ( $properties )
Constructor. Builds a new, default cart object in Moose style with default properties set by $properties. This does not
persist them to the database automatically. This needs to be done via $self->write.
=head3 $session
A reference to the current session.
=head3 $cartId
The unique id of a cart to instanciate.
=head3 $properties
A hash reference that contains one or more of the following:
=head4 shippingAddressId
The unique id for a shipping address attached to this cart.
=head4 billingAddressId
The unique id for a billing address attached to this cart.
=head4 shipperId
The unique id of the configured shipping driver that will be used to ship these goods.
=head4 posUserId
The ID of a user being checked out, if they're being checked out by a cashier.
=head4 creationDate
The date the cart was created.
=cut
around BUILDARGS => sub {
my $orig = shift;
my $class = shift;
if (ref $_[0] eq 'HASH') {
my $properties = $_[0];
my $session = $properties->{session};
if (! (blessed $session && $session->isa('WebGUI::Session')) ) {
WebGUI::Error::InvalidObject->throw(expected=>"WebGUI::Session", got=>(ref $session), error=>"Need a session.");
}
my ($cartId, $creationDate) = $class->_init($session);
$properties->{cartId} = $cartId;
$properties->{creationDate} = $creationDate;
return $class->$orig($properties);
}
my $session = shift;
if (! (blessed $session && $session->isa('WebGUI::Session'))) {
WebGUI::Error::InvalidObject->throw(expected=>"WebGUI::Session", got=>(ref $session), error=>"Need a session.");
}
my $argument2 = shift;
if (!defined $argument2) {
my ($cartId, $creationDate) = $class->_init($session);
my $properties = {};
$properties->{session} = $session;
$properties->{cartId} = $cartId;
$properties->{creationDate} = $creationDate;
return $class->$orig($properties);
}
##Look up one in the db
my $cart = $session->db->quickHashRef("select * from cart where cartId=?", [$argument2]);
if ($cart->{cartId} eq "") {
WebGUI::Error::ObjectNotFound->throw(error=>"No such cart.", id=>$argument2);
}
$cart->{session} = $session;
return $class->$orig($cart);
};
#-------------------------------------------------------------------
=head2 _init ( session )
Builds a stub of object information in the database, and returns the newly created
cartId, and the creationDate fields so the object can be initialized correctly.
=cut
sub _init {
my $class = shift;
my $session = shift;
my $creationDate = WebGUI::DateTime->new($session)->epoch;
my $cartId = $session->id->generate;
$session->db->write('insert into cart (cartId, sessionId, creationDate) values (?,?,?)', [$cartId, $session->getId, $creationDate]);
return ($cartId, $creationDate);
}
#-------------------------------------------------------------------
=head2 addItem ( sku )
Adds an item to the cart. Returns a reference to the newly added item.
=head3 sku
A reference to a subclass of WebGUI::Asset::Sku.
=cut
sub addItem {
my ($self, $sku) = @_;
unless (defined $sku && $sku->isa("WebGUI::Asset::Sku")) {
WebGUI::Error::InvalidObject->throw(expected=>"WebGUI::Asset::Sku", got=>(ref $sku), error=>"Need a sku.");
}
my $item = WebGUI::Shop::CartItem->new($self, $sku);
$item->write();
$sku->onAdjustQuantityInCart($item, 1);
return $item;
}
#-------------------------------------------------------------------
=head2 calculateShopCreditDeduction ( [ total ] )
Returns the amount of the total that will be deducted by shop credit.
=head3 total
The amount to calculate the deduction against. Defaults to calculateTotal().
=cut
sub calculateShopCreditDeduction {
my ($self, $total) = @_;
unless (defined $total) {
$total = $self->calculateTotal
}
# cannot use in-shop credit on recurring items
return $self->formatCurrency(0) if $self->requiresRecurringPayment;
return $self->formatCurrency(WebGUI::Shop::Credit->new($self->session, $self->posUserId)->calculateDeduction($total));
}
#-------------------------------------------------------------------
=head2 calculateShipping ()
Returns the cost of shipping for the cart.
=cut
sub calculateShipping {
my $self = shift;
# get the shipper
my $shipper = eval { $self->getShipper };
# can't calculate shipping price without a valid shipper
if (WebGUI::Error->caught) {
return $self->formatCurrency(0);
}
# do calculation
return $self->formatCurrency($shipper->calculate($self));
}
#-------------------------------------------------------------------
=head2 calculateSubtotal ()
Returns the subtotal of the items in the cart.
=cut
sub calculateSubtotal {
my $self = shift;
my $subtotal = 0;
foreach my $item (@{$self->getItems}) {
my $sku = $item->getSku;
$subtotal += $sku->getPrice * $item->get("quantity");
}
return $subtotal;
}
#-------------------------------------------------------------------
=head2 calculateTaxes ()
Returns the tax amount on the items in the cart.
=cut
sub calculateTaxes {
my $self = shift;
my $tax = WebGUI::Shop::Tax->new($self->session);
return $self->formatCurrency($tax->calculate($self));
}
#-------------------------------------------------------------------
=head2 calculateTotal ( )
Returns the total price of everything in the cart including tax, shipping, etc.
=cut
sub calculateTotal {
my ($self) = @_;
return $self->calculateSubtotal + $self->calculateShipping + $self->calculateTaxes;
}
#-------------------------------------------------------------------
=head2 create ( session )
Deprecated, left as a stub for existing code. Use L<new> instead.
=head3 session
A reference to the current session.
=cut
sub create {
my ($class, $session) = @_;
return $class->new($session);
}
#-------------------------------------------------------------------
=head2 delete ()
Deletes this cart and removes all cartItems contained in it. Also see onCompletePurchase() and empty().
=cut
sub delete {
my ($self) = @_;
$self->empty;
$self->session->db->write("delete from cart where cartId=?",[$self->getId]);
return undef;
}
#-------------------------------------------------------------------
=head2 empty ()
Removes all items from this cart. Also see onCompletePurchase() and delete().
=cut
sub empty {
my ($self) = @_;
foreach my $item (@{$self->getItems}) {
$item->remove;
}
}
#-------------------------------------------------------------------
=head2 formatCurrency ( amount )
Formats a number as a float with two digits after the decimal like 0.00.
=head3 amount
The number to format.
=cut
sub formatCurrency {
my ($self, $amount) = @_;
unless (defined $amount) {
WebGUI::Error::InvalidParam->throw(error=>"Need an amount.");
}
return sprintf("%.2f", $amount);
}
#-------------------------------------------------------------------
=head2 getAddressBook ()
Returns a reference to the address book for the user who's cart this is.
=cut
sub getAddressBook {
my $self = shift;
unless (exists $self->{_addressBook}) {
$self->{_addressBook} = WebGUI::Shop::AddressBook->newByUserId($self->session);
}
return $self->{_addressBook};
}
#-------------------------------------------------------------------
=head2 getBillingAddress ()
Returns the WebGUI::Shop::Address object that is attached to this cart for billing.
=cut
sub getBillingAddress {
my $self = shift;
my $book = $self->getAddressBook;
if (my $addressId = $self->billingAddressId) {
return $book->getAddress($addressId);
}
my $address = $book->getDefaultAddress;
$self->update({billingAddressId=>$address->getId});
return $address;
}
#-------------------------------------------------------------------
=head2 getI18nError ()
Returns an internationalized version of the current error, if it exists.
=cut
sub getI18nError {
my ($self) = @_;
my $error = $self->error;
my $i18n = WebGUI::International->new($self->session, 'Shop');
return $error eq 'no billing address' ? $i18n->get('no billing address')
: $error eq 'no shipping address' ? $i18n->get('no shipping address')
: $error eq 'billing firstName' ? $i18n->get('billing firstName')
: $error eq 'billing lastName' ? $i18n->get('billing lastName')
: $error eq 'billing address1' ? $i18n->get('billing address1')
: $error eq 'billing city' ? $i18n->get('billing city')
: $error eq 'billing code' ? $i18n->get('billing code')
: $error eq 'billing state' ? $i18n->get('billing state')
: $error eq 'billing country' ? $i18n->get('billing country')
: $error eq 'billing phoneNumber' ? $i18n->get('billing phoneNumber')
: $error eq 'shipping firstName' ? $i18n->get('shipping firstName')
: $error eq 'shipping lastName' ? $i18n->get('shipping lastName')
: $error eq 'shipping address1' ? $i18n->get('shipping address1')
: $error eq 'shipping city' ? $i18n->get('shipping city')
: $error eq 'shipping code' ? $i18n->get('shipping code')
: $error eq 'shipping state' ? $i18n->get('shipping state')
: $error eq 'shipping country' ? $i18n->get('shipping country')
: $error eq 'shipping phoneNumber' ? $i18n->get('shipping phoneNumber')
: $error eq 'no shipping method set' ? $i18n->get('Choose a shipping method and update the cart to checkout')
: $error eq 'no payment gateway set' ? $i18n->get('Choose a payment gateway and update the cart to checkout')
: $error ;
}
#-------------------------------------------------------------------
=head2 getId ()
Returns the unique id for this cart.
=cut
sub getId {
my ($self) = @_;
return $self->cartId;
}
#-------------------------------------------------------------------
=head2 getItem ( itemId )
Returns a reference to a WebGUI::Shop::CartItem object. Throws an WebGUI::Error::InvalidParam
exception if no itemId is passed, or if an invalid itemId is passed. It will not catch any
exceptions thrown by actually creating the CartItem, the caller of this method should do that.
=head3 itemId
The id of the item to retrieve.
=cut
sub getItem {
my ($self, $itemId) = @_;
unless (defined $itemId && $self->session->id->valid($itemId)) {
WebGUI::Error::InvalidParam->throw(error=>"Need an itemId.");
}
my $item = WebGUI::Shop::CartItem->new($self, $itemId);
return $item;
}
#-------------------------------------------------------------------
=head2 getItems ( )
Returns an array reference of WebGUI::Asset::Sku objects that are in the cart.
=cut
sub getItems {
my ($self) = @_;
my @itemsObjects = ();
my $items = $self->session->db->read("select itemId from cartItem where cartId=?",[$self->getId]);
while (my ($itemId) = $items->array) {
push(@itemsObjects, $self->getItem($itemId));
}
return \@itemsObjects;
}
#-------------------------------------------------------------------
=head2 getItemsByAssetId ( assetIds )
Returns an array reference of WebGUI::Asset::Sku objects that have a specific asset id that are in the cart.
=head3 assetIds
An array reference of assetIds to look for.
=cut
sub getItemsByAssetId {
my ($self, $assetIds) = @_;
return [] unless (scalar(@{$assetIds}) > 0);
my @itemsObjects = ();
my $items = $self->session->db->read("select itemId from cartItem where cartId=? and assetId in (".$self->session->db->quoteAndJoin($assetIds).")",[$self->getId]);
while (my ($itemId) = $items->array) {
push(@itemsObjects, $self->getItem($itemId));
}
return \@itemsObjects;
}
#-------------------------------------------------------------------
=head2 getPaymentGateway ()
Returns the WebGUI::Shop::PayDriver object that is attached to this cart for payment.
=cut
sub getPaymentGateway {
my $self = shift;
return WebGUI::Shop::Pay->new($self->session)->getPaymentGateway($self->gatewayId);
}
#-------------------------------------------------------------------
=head2 getPosUser
Returns the userId of the user making a purchase. If there is a cashier and the cashier has specified a user, then that user will be returned. Otherwise, if it's a direct sale then $session->user will be returned.
=cut
sub getPosUser {
my $self = shift;
if ($self->posUserId ne "") {
return WebGUI::User->new($self->session, $self->get('posUserId'));
}
return $self->session->user;
}
#-------------------------------------------------------------------
=head2 getShipper ()
Returns the WebGUI::Shop::ShipDriver object that is attached to this cart for shipping.
=cut
sub getShipper {
my $self = shift;
return WebGUI::Shop::Ship->new(session => $self->session)->getShipper($self->shipperId);
}
#-------------------------------------------------------------------
=head2 getShippingAddress ()
Returns the WebGUI::Shop::Address object that is attached to this cart for shipping.
=cut
sub getShippingAddress {
my $self = shift;
my $book = $self->getAddressBook;
if ($self->shippingAddressId) {
return $book->getAddress($self->shippingAddressId);
}
my $address = $book->getDefaultAddress;
$self->update({shippingAddressId=>$address->getId});
return $address;
}
#-------------------------------------------------------------------
=head2 hasMixedItems ()
Returns 1 if there are too many recurring items, or there are mixed recurring and non-recurring items in the cart.
=cut
sub hasMixedItems {
my $self = shift;
my $recurring = 0;
my $nonrecurring = 0;
foreach my $item (@{$self->getItems}) {
if ($item->getSku->isRecurring) {
$recurring += $item->get('quantity');
}
else {
$nonrecurring += $item->get('quantity');
}
return 1 if ($recurring > 0 && $nonrecurring > 0);
return 1 if ($recurring > 1);
}
return 0;
}
#-------------------------------------------------------------------
=head2 newBySession ( session )
Class method that figures out if the user has a cart in their session. If they do it returns it. If they don't it creates it and returns it.
=head3 session
A reference to the current session.
=cut
sub newBySession {
my ($class, $session) = @_;
unless (defined $session && $session->isa("WebGUI::Session")) {
WebGUI::Error::InvalidObject->throw(expected=>"WebGUI::Session", got=>(ref $session), error=>"Need a session.");
}
my $cartId = $session->db->quickScalar("select cartId from cart where sessionId=?",[$session->getId]);
return $class->new($session, $cartId); ##Falls back to creating a new cart if there's no 2nd argument
}
#-------------------------------------------------------------------
=head2 onCompletePurchase ()
Deletes all the items in the cart without calling $item->remove() on them which would affect inventory levels. See also delete() and empty().
=cut
sub onCompletePurchase {
my $self = shift;
foreach my $item (@{$self->getItems}) {
$item->delete;
}
$self->delete;
}
#-------------------------------------------------------------------
=head2 readyForCheckout ( )
Returns whether all the required properties of the the cart are set.
=cut
sub readyForCheckout {
my $self = shift;
my $session = $self->session;
my $book = $self->getAddressBook;
# Check if the billing address is set and correct
my $address = eval{$self->getBillingAddress};
if (WebGUI::Error->caught) {
$self->error('no billing address');
return 0;
}
if (my @missingFields = $book->missingFields($address->get)) {
$self->error('billing '.$missingFields[0]);
return 0;
}
# Check if the shipping address is set and correct
my $shipAddress = eval{$self->getShippingAddress};
if (WebGUI::Error->caught) {
$self->error('no shipping address');
return 0;
}
if (my @missingFields = $book->missingFields($shipAddress->get)) {
$self->error('shipping '.$missingFields[0]);
return 0;
}
if ($self->requiresShipping) {
##Must have a configured shipping id.
if (! $self->shipperId) {
$self->error('no shipping method set');
return 0;
}
my $shipper = eval { WebGUI::Shop::ShipDriver->new($session, $self->shipperId)};
if (my $e = WebGUI::Error->caught) {
$self->error($e->error);
return 0;
}
}
# Check if the cart has items
return 0 unless scalar @{ $self->getItems };
# fail if there are multiple recurring items
return 0 if ($self->hasMixedItems);
# Check minimum cart checkout requirement
my $total = eval { $self->calculateTotal };
if (my $e = WebGUI::Error->caught) {
$self->error($e->error);
return 0;
}
my $requiredAmount = $self->session->setting->get( 'shopCartCheckoutMinimum' );
if ( $requiredAmount > 0 && $total < $requiredAmount) {
$self->error('required amount not met in cart');
return 0;
}
##Must have a configured payment method.
if (! $self->gatewayId) {
$self->error('no payment gateway set');
return 0;
}
my $gateway = eval { WebGUI::Shop::PayDriver->new($session, $self->gatewayId)};
if (my $e = WebGUI::Error->caught) {
$self->error($e->error);
return 0;
}
##Check for any other logged errors
return 0 if $self->error;
# All checks passed so return true
return 1;
}
#-------------------------------------------------------------------
=head2 requiresRecurringPayment ( )
Returns whether this cart needs to be checked out with a paydriver that can handle recurring payments.
=cut
sub requiresRecurringPayment {
my $self = shift;
# Look for recurring items in the cart
foreach my $item (@{ $self->getItems }) {
return 1 if $item->getSku->isRecurring;
}
# No recurring items in cart so return false
return 0;
}
#-------------------------------------------------------------------
=head2 requiresShipping ( )
Returns whether any item in this cart requires shipping.
=cut
sub requiresShipping {
my $self = shift;
# Look for recurring items in the cart
ITEM: foreach my $item (@{ $self->getItems }) {
my $sku = $item->getSku;
next ITEM unless $sku;
return 1 if $sku->isShippingRequired;
}
# No recurring items in cart so return false
return 0;
}
#-------------------------------------------------------------------
=head2 write ( )
Serialize the current set of cart properties to the database.
=cut
sub write {
my ($self) = @_;
my $properties = $self->get();
delete $properties->{error};
$self->session->db->setRow("cart", "cartId", $properties);
}
#-------------------------------------------------------------------
=head2 updateFromForm ( )
Updates the cart totals, the address fields and the shipping and billing options from form data.
=cut
sub updateFromForm {
my $self = shift;
my $session = $self->session;
my $form = $session->form;
foreach my $item (@{$self->getItems}) {
if ($form->get("quantity-".$item->getId) ne "") {
eval { $item->setQuantity($form->get("quantity-".$item->getId)) };
if (WebGUI::Error->caught("WebGUI::Error::Shop::MaxOfItemInCartReached")) {
my $i18n = WebGUI::International->new($self->session, "Shop");
$self->error(sprintf($i18n->get("too many of this item"), $item->get("configuredTitle")));
}
elsif (my $e = WebGUI::Error->caught) {
$self->error("An unknown error has occured: ".$e->message);
}
}
if (my $itemAddressId = $form->get("itemAddress_".$item->getId)) {
$item->update({shippingAddressId => $itemAddressId});
}
}
if ($self->hasMixedItems) {
my $i18n = WebGUI::International->new($self->session, "Shop");
$self->error($i18n->get('mixed items warning'));
}
my @cartItemIds = $form->process('remove_item', 'checkList');
foreach my $cartItemId (@cartItemIds) {
my $item = eval { $self->getItem($cartItemId); };
$item->remove if ! Exception::Class->caught();
}
##Visitor cannot have an address book, or set a payment gateway, so skip the rest of this.
return 1 if $session->user->isVisitor;
my $book = $self->getAddressBook;
my $cartProperties = {};
my %billingData = $book->processAddressForm('billing_');
my @missingBillingFields = $book->missingFields(\%billingData);
my $billingAddressId = $form->process('billingAddressId');
if ($billingAddressId eq 'new_address' && ! @missingBillingFields) {
##Add a new address
my $newAddress = $book->addAddress(\%billingData);
$cartProperties->{billingAddressId} = $newAddress->get('addressId');
}
elsif ($billingAddressId eq 'update_address' && $self->billingAddressId && ! @missingBillingFields) {
##User updated the current address
my $address = $self->getBillingAddress();
$address->update(\%billingData);
}
elsif ($billingAddressId ne 'new_address' && $billingAddressId ne 'update_address' && $billingAddressId) {
##User changed the address selector to another address field
$cartProperties->{billingAddressId} = $billingAddressId;
}
elsif (@missingBillingFields) {
$self->error('billing '.$missingBillingFields[0]);
}
else {
$self->session->log->warn('billing address: something else: ');
}
##Update now, so that you can add an address AND set the shipping address to be the same at the same time.
$self->update($cartProperties);
if ($self->requiresShipping) {
if ($form->process('sameShippingAsBilling', 'yesNo')) {
$cartProperties->{shippingAddressId} = $self->billingAddressId;
}
else {
my %shippingData = $book->processAddressForm('shipping_');
my @missingShippingFields = $book->missingFields(\%shippingData);
my $shippingAddressId = $form->process('shippingAddressId');
##No missing shipping fields, if we set to the same as the billing fields
if (@missingShippingFields) {
$self->error('shipping '.$missingShippingFields[0]);
}
if ($shippingAddressId eq 'new_address' && ! @missingShippingFields) {
##Add a new address
my $newAddress = $book->addAddress(\%shippingData);
$cartProperties->{shippingAddressId} = $newAddress->get('addressId');
}
elsif ($shippingAddressId eq 'update_address' && $self->shippingAddressId && ! @missingShippingFields) {
##User changed the address selector
my $address = $self->getBillingAddress();
$address->update(\%shippingData);
}
elsif ($shippingAddressId ne 'new_address' && $shippingAddressId ne 'update_address' && $shippingAddressId) {
$cartProperties->{shippingAddressId} = $shippingAddressId;
}
else {
$self->session->log->warn('shipping address: something else: ');
}
}
}
$cartProperties->{ shipperId } = $form->process( 'shipperId' ) if $form->process( 'shipperId' );
$cartProperties->{ gatewayId } = $form->process( 'gatewayId' ) if $form->process( 'gatewayId' );
$self->update( $cartProperties );
}
#-------------------------------------------------------------------
=head2 www_ajaxPrices
Input: shippingId (an addressId)
billingId (an addressId)
Output: {
tax : 1.25,
subtotal : 12.00,
shipping : {
slfjaldjfalfja: {
label : 'USPS',
price : 12.50,
hasPrice : 1 || 0
},
{ ... },
{ ... }
}
}
Takes an addressId and returns JSON shipping options for it, in the form .
=cut
sub www_ajaxPrices {
my $self = shift;
my $session = $self->session;
my $form = $session->form;
my $billing = $form->get('billingId');
my $shipping = $form->get('shippingId');
my $response = {
subtotal => $self->calculateSubtotal(),
tax => eval {
my $addr = $shipping || $billing or die;
$self->update({ shippingAddressId => $addr });
$self->calculateTaxes();
} || 0,
shipping => eval {
#die unless $shipping;
if ( $shipping ) {
$self->update({ shippingAddressId => $shipping });
}
my $ship = WebGUI::Shop::Ship->new($self->session);
$ship->getOptions($self);
} || [],
};
$session->response->content_type('text/plain');
return JSON->new->encode($response);
}
#-------------------------------------------------------------------
=head2 www_ajaxSetCartItemShippingId
Sets the shippingAddressId for a particular cartItem
=cut
sub www_ajaxSetCartItemShippingId {
my $self = shift;
my $session = $self->session;
my $form = $session->form;
my $item = $self->getItem($form->get('itemId'));
my $address = $form->get('addressId') || undef;
$item && $item->update({ shippingAddressId => $address });
$session->response->content_type('text/plain');
return 'ok';
}
#-------------------------------------------------------------------
=head2 www_checkout ( )
All checks and work for checking out are contained in here.
=cut
sub www_checkout {
my $self = shift;
my $session = $self->session;
##Setting a shipping address greatly simplifies the Transaction
if (! $self->requiresShipping && ! $self->shippingAddressId) {
$self->update({shippingAddressId => $self->billingAddressId});
}
if ($self->readyForCheckout()) {
my $total = $self->calculateTotal;
##Handle rounding errors, and checkout immediately if the amount is 0 since
##at least the ITransact driver won't accept $0 checkout.
if (sprintf('%.2f', abs($total + $self->calculateShopCreditDeduction($total))) eq '0.00') {
my $transaction = WebGUI::Shop::Transaction->new({session => $session, cart => $self});
$transaction->write;
$transaction->completePurchase('zero', 'success', 'success');
$self->onCompletePurchase;
$transaction->sendNotifications();
return $transaction->thankYou();
}
my $gateway = $self->getPaymentGateway;
return $gateway->www_getCredentials;
}
return $self->www_view;
}
#-------------------------------------------------------------------
=head2 www_lookupPosUser ( )
Adds a Point of Sale user to the cart.
=cut
sub www_lookupPosUser {
my $self = shift;
my $session = $self->session;
my $email = $session->form->get('posEmail','email');
my $user = WebGUI::User->newByEmail($session, $email);
unless (defined $user) {
$user = WebGUI::User->newByUsername($session, $email);
unless (defined $user) {
$user = WebGUI::User->new($session, "new");
$user->username($email);
$user->get('email', $email);
}
}
$self->update({posUserId=>$user->userId});
return $self->www_view;
}
#-------------------------------------------------------------------
=head2 www_update ( )
Updates the cart totals, addresses, shipping driver and payment gateway. If requested, and the
cart is ready, calls the www_getCredentials method from the selected payment gateway.
Otherwise, returns the user back to the cart.
=cut
sub www_update {
my $self = shift;
my $session = $self->session;
$self->updateFromForm;
if ($session->form->get('continue_shopping')) {
return undef;
}
elsif ($session->form->get('checkout')) {
##Shortcut method for checkout, so that the cart form contents don't get lost.
return $self->www_checkout();
}
return $self->www_view;
}
#-------------------------------------------------------------------
=head2 www_view ( )
Displays the shopping cart.
=cut
sub www_view {
my $self = shift;
my $session = $self->session;
my $url = $session->url;
my $form = $session->form;
my $i18n = WebGUI::International->new($session, "Shop");
my $taxDriver = WebGUI::Shop::Tax->getDriver( $session );
if($url->forceSecureConnection()){
return "redirect";
}
my @cartItems = @{$self->getItems};
if(scalar(@cartItems) < 1) {
# there are no items in the cart, return a message to the template
my %var = (
message => $i18n->get('empty cart')
);
# render the cart
my $template = WebGUI::Asset::Template->newById($session, $session->setting->get("shopCartTemplateId"));
return $session->style->userStyle($template->process(\%var));
}
my %var = (
%{$self->get},
formHeader => WebGUI::Form::formHeader($session, { extras => q|id="wgCartId"|, })
. WebGUI::Form::hidden($session, {name=>"shop", value=>"cart"})
. WebGUI::Form::hidden($session, {name=>"method", value=>"update"})
. WebGUI::Form::hidden($session, {name=>"itemId", value=>""})
,
formFooter => WebGUI::Form::formFooter($session),
updateButton => WebGUI::Form::submit($session, {value=>$i18n->get("update cart button"), id=>"updateCartButton"}),
checkoutButton => WebGUI::Form::submit($session, {name => 'checkout', value=>$i18n->get("checkout button"), id=>"checkoutButton"}),
continueShoppingButton => WebGUI::Form::submit($session, {
name => 'continue_shopping',
value => $i18n->get("continue shopping button"),
id => 'continueShoppingButton',
}),
minimumCartAmount => $session->setting->get( 'shopCartCheckoutMinimum' ) > 0
? sprintf( '%.2f', $session->setting->get( 'shopCartCheckoutMinimum' ) )
: 0
,
userIsVisitor => $session->user->isVisitor,
shippableItemsInCart => $self->requiresShipping,
);
# get the shipping address
my $address = eval { $self->getShippingAddress };
if (my $e = WebGUI::Error->caught("WebGUI::Error::ObjectNotFound") && $self->shippingAddressId) {
# choose another address cuz we've got a problem
$self->update({shippingAddressId=>''});
}
#get the billing address
my $billingAddress = eval { $self->getBillingAddress };
if (my $e = WebGUI::Error->caught("WebGUI::Error::ObjectNotFound") && $self->get('billingAddressId')) {
# choose another address cuz we've got a problem
$self->update({billingAddressId=>''});
}
# generate template variables for the items in the cart
my @items = ();
tie my %addressOptions, 'Tie::IxHash';
if (! $var{userIsVisitor}) {
my $addressBook = $self->getAddressBook;
my $addresses = $addressBook->getAddresses;
foreach my $address (@{ $addresses }) {
$addressOptions{$address->get('addressId')} = $address->get('label');
}
}
tie my %specialAddressOptions, 'Tie::IxHash';
$specialAddressOptions{''} = $i18n->get("Special shipping");
%specialAddressOptions = (%specialAddressOptions, %addressOptions);
foreach my $item (@cartItems) {
my $sku = $item->getSku;
$sku->applyOptions($item->get("options"));
my %properties = (
%{$item->get},
url => $sku->getUrl("shop=cart;method=viewItem;itemId=".$item->getId),
quantityField => WebGUI::Form::integer($session, {name=>"quantity-".$item->getId, value=>$item->get("quantity"), size=>5,}),
isUnique => ($sku->getMaxAllowedInCart == 1),
isShippable => $sku->isShippingRequired,
extendedPrice => $self->formatCurrency($sku->getPrice * $item->get("quantity")),
price => $self->formatCurrency($sku->getPrice),
removeBox => WebGUI::Form::checkbox($session, {name => 'remove_item', value => $item->get('itemId')}),
);
my $itemAddress = eval {$item->getShippingAddress};
my $itemAddressId;
if ((!WebGUI::Error->caught) && $itemAddress && $address && $itemAddress->getId ne $address->getId) {
$properties{shippingAddress} = $itemAddress->getHtmlFormatted;
$itemAddressId = $itemAddress->getId;
}
else {
$properties{shippingAddress} = '';
$itemAddressId = '';
}
if (! $var{userIsVisitor} && @cartItems > 1) {
$properties{itemAddressChooser} = WebGUI::Form::selectBox($session, {
name => "itemAddress_".$item->getId,
value => $itemAddressId,
options => \%specialAddressOptions,
extras => q|class="itemAddressMenu"|,
});
$taxDriver->appendCartItemVars( \%properties, $item );
}
push(@items, \%properties);
}
$var{items} = \@items;
if (! $var{userIsVisitor} && $var{shippableItemsInCart}) {
my $ship = WebGUI::Shop::Ship->new($self->session);
my $options = $ship->getOptions($self);
my $numberOfOptions = scalar keys %{ $options };
if (! $numberOfOptions) {
$var{shippingOptions} = '';
$var{shippingPrice} = 0;
$self->error($i18n->get("No shipping plugins configured"));
}
else {
tie my %formOptions, 'Tie::IxHash';
$formOptions{''} = $i18n->get('Choose a shipping method');
foreach my $optionId (keys %{$options}) {
$formOptions{$optionId} = $options->{$optionId}{label};
if ($options->{$optionId}->{hasPrice}) {
$formOptions{$optionId} .= ' ('.$self->formatCurrency($options->{$optionId}{price}).')';
}
}
my $shipperId = $self->shipperId;
if (!$shipperId && $numberOfOptions == 1) {
my ($option) = keys %{ $options };
$self->update({shipperId => $option});
$shipperId = $option;
}
$var{shippingOptions} = WebGUI::Form::selectBox($session, {name=>"shipperId", options=>\%formOptions, value=>$shipperId || ''});
if (!exists $options->{$shipperId}) {
$self->update({shipperId => ''});
$shipperId = '';
}
if ($shipperId) {
$var{shippingPrice} = $options->{$shipperId}->{price};
}
else {
$var{shippingPrice} = 0;
$self->error($i18n->get('Choose a shipping method and update the cart to checkout'));
}
$var{shippingPrice} = $shipperId && $options->{$shipperId}->{hasPrice} ? $self->formatCurrency($var{shippingPrice}) : '';
$var{tax} = $self->calculateTaxes;
}
}
else {
$var{shippingPrice} = $var{tax} = $self->formatCurrency(0);
}
# Tax variables
#Address form variables
if ($var{userIsVisitor}) {
$var{loginFormHeader} = WebGUI::Form::formHeader($session, {action => $session->url->page})
. WebGUI::Form::hidden($session,{ name => 'op', value => 'auth'})
. WebGUI::Form::hidden($session,{ name => 'method', value => 'login'})
;
$var{loginFormUsername} = WebGUI::Form::text($session, { name => 'username', size => 12, });
$var{loginFormPassword} = WebGUI::Form::password($session, { name => 'identifier', size => 12, });
$var{loginFormButton} = WebGUI::Form::submit($session, { value => $i18n->get(52,'WebGUI'), });
$var{registerLink} = $session->url->page('op=auth;method=createAccount');
$session->scratch->set('redirectAfterLogin', $session->url->page('shop=cart'));
$var{loginFormFooter} = WebGUI::Form::formFooter($session)
}
else {
##Address form variables
tie my %billingAddressOptions, 'Tie::IxHash';
$billingAddressOptions{'new_address'} = $i18n->get('Add new address');
my $billingAddressId = $self->billingAddressId;
if ($billingAddressId) {
my $billingAddress = eval { $self->getBillingAddress };
if ( defined $billingAddress ) {
$billingAddressOptions{'update_address'} = sprintf $i18n->get('Update %s'), $billingAddress->get('label');
}
elsif (my $e = WebGUI::Error->caught("WebGUI::Error::ObjectNotFound") && $self->get('billingAddressId')) {
$self->update({billingAddressId=>''});
}
}
%billingAddressOptions = (%billingAddressOptions, %addressOptions);
$var{'billingAddressChooser'} = WebGUI::Form::selectBox($session, {
name => 'billingAddressId',
options => \%billingAddressOptions,
value => $billingAddressId ? $billingAddressId : 'new_address',
});
tie my %shippingAddressOptions, 'Tie::IxHash';
$shippingAddressOptions{'new_address'} = $i18n->get('Add new address');
my $shippingAddressId = $self->shippingAddressId;
if ($shippingAddressId) {
my $shippingAddress = eval { $self->getShippingAddress };
if ( defined $shippingAddress ) {
$shippingAddressOptions{'update_address'} = sprintf $i18n->get('Update %s'), $shippingAddress->get('label');
}
}
%shippingAddressOptions = (%shippingAddressOptions, %addressOptions);
$var{'shippingAddressChooser'} = WebGUI::Form::selectBox($session, {
name => 'shippingAddressId',
options => \%shippingAddressOptions,
value => $shippingAddressId ? $shippingAddressId : 'new_address',
});
my $shippingAddressData = $self->shippingAddressId ? $self->getShippingAddress->get() : {};
my $billingAddressData = $self->billingAddressId ? $self->getBillingAddress->get() : {};
my $addressBook = $self->getAddressBook;
$addressBook->appendAddressFormVars(\%var, 'shipping_', $shippingAddressData);
$addressBook->appendAddressFormVars(\%var, 'billing_', $billingAddressData);
my $has_billing_addr = $self->get('billingAddressId') ? 1 : 0;
$var{sameShippingAsBilling} = WebGUI::Form::yesNo($session, {
name => 'sameShippingAsBilling',
value => (($has_billing_addr && $self->billingAddressId eq $self->shippingAddressId) || !$has_billing_addr),
});
}
# Payment methods
my $pay = WebGUI::Shop::Pay->new($session);
tie my %paymentOptions, 'Tie::IxHash';
$paymentOptions{''} = $i18n->get('Choose a payment method');
my $gateways = $pay->getOptions($self);
while (my ($gatewayId, $label) = each %{ $gateways }) {
$paymentOptions{$gatewayId} = $label;
}
$var{paymentOptions} = WebGUI::Form::selectBox($session, {
name => 'gatewayId',
options => \%paymentOptions,
value => $self->gatewayId || $form->get('gatewayId') || '',
});
# POS variables
$var{isCashier} = WebGUI::Shop::Admin->new($session)->isCashier;
$var{posLookupForm} = WebGUI::Form::email($session, {name=>"posEmail"})
.WebGUI::Form::submit($session, {value=>$i18n->get('search for email'),
extras=>q|onclick="this.form.method.value='lookupPosUser';this.form.submit;"|});
my $posUser = $self->getPosUser;
$var{posUsername} = $posUser->username;
$var{posUserId} = $posUser->userId;
# calculate price adjusted for in-store credit
$var{subtotalPrice} = $self->formatCurrency($self->calculateSubtotal());
$var{totalPrice} = $var{subtotalPrice} + $var{shippingPrice} + $var{tax};
my $credit = WebGUI::Shop::Credit->new($session, $posUser->userId);
$var{ inShopCreditAvailable } = $credit->getSum;
$var{ inShopCreditDeduction } = $self->calculateShopCreditDeduction($var{totalPrice});
$var{ totalPrice } = $self->formatCurrency($var{totalPrice} + $var{inShopCreditDeduction});
foreach my $field (qw/subtotalPrice inShopCreditAvailable inShopCreditDeduction totalPrice shippingPrice tax/) {
$var{$field} = sprintf q|<span id="%sWrap">%s</span>|, $field, $var{$field};
}
$var{ error } = $self->getI18nError;
# render the cart
my $template = WebGUI::Asset->newById($session, $session->setting->get("shopCartTemplateId"));
my $style = $session->style;
my $yui = $url->extras('/yui/build');
$style->setScript("$yui/yahoo-dom-event/yahoo-dom-event.js");
$style->setScript("$yui/json/json-min.js");
$style->setScript("$yui/connection/connection-min.js");
$style->setScript($url->extras('underscore/underscore-min.js'));
$style->setScript($url->extras('shop/cart.js'), undef, 1);
return $session->style->userStyle($template->process(\%var));
}
#-------------------------------------------------------------------
=head2 www_viewItem ( )
Displays the configured item.
=cut
sub www_viewItem {
my $self = shift;
my $itemId = $self->session->form->get("itemId");
my $item = eval { $self->getItem($itemId) };
if (WebGUI::Error->caught()) {
return $self->www_view;
}
return $item->getSku->www_view;
}
1;