diff --git a/docs/upgrades/packages-7.10.22/shopping-cart-collateral-items.wgpkg b/docs/upgrades/packages-7.10.22/shopping-cart-collateral-items.wgpkg new file mode 100644 index 000000000..b48ee8eb8 Binary files /dev/null and b/docs/upgrades/packages-7.10.22/shopping-cart-collateral-items.wgpkg differ diff --git a/docs/upgrades/upgrade_7.10.21-7.10.22.pl b/docs/upgrades/upgrade_7.10.21-7.10.22.pl index 660f8b034..1ee778a94 100644 --- a/docs/upgrades/upgrade_7.10.21-7.10.22.pl +++ b/docs/upgrades/upgrade_7.10.21-7.10.22.pl @@ -31,6 +31,7 @@ my $quiet; # this line required my $session = start(); # this line required # upgrade functions go here +addAuthorizePaymentDriver($session); finish($session); # this line required @@ -44,6 +45,16 @@ finish($session); # this line required # print "DONE!\n" unless $quiet; #} +#---------------------------------------------------------------------------- +# Add the Authorize.net payment driver to each config file +sub addAuthorizePaymentDriver { + my $session = shift; + print "\tAdd the Authorize.net payment driver... " unless $quiet; + # and here's our code + $session->config->addToArray('paymentDrivers', 'WebGUI::Shop::PayDriver::CreditCard::AuthorizeNet'); + print "DONE!\n" unless $quiet; +} + # -------------- DO NOT EDIT BELOW THIS LINE -------------------------------- diff --git a/etc/WebGUI.conf.original b/etc/WebGUI.conf.original index 806a9d94b..0371550b7 100644 --- a/etc/WebGUI.conf.original +++ b/etc/WebGUI.conf.original @@ -200,6 +200,7 @@ "WebGUI::Shop::PayDriver::ITransact", "WebGUI::Shop::PayDriver::PayPal::PayPalStd", "WebGUI::Shop::PayDriver::PayPal::ExpressCheckout", + "WebGUI::Shop::PayDriver::CreditCard::AuthorizeNet", "WebGUI::Shop::PayDriver::Ogone" ], diff --git a/lib/WebGUI/Shop/PayDriver/CreditCard.pm b/lib/WebGUI/Shop/PayDriver/CreditCard.pm new file mode 100644 index 000000000..fe41ac590 --- /dev/null +++ b/lib/WebGUI/Shop/PayDriver/CreditCard.pm @@ -0,0 +1,240 @@ +package WebGUI::Shop::PayDriver::CreditCard; + +use strict; +use Readonly; + +=head1 NAME + +WebGUI::Shop::PayDriver::CreditCard + +=head2 DESCRIPTION + +A base class for credit card payment drivers. They all need pretty much the +same information, the only difference is the servers you talk to. Leaves you +to handle recurring payments, processPayment, www_edit, and whatever else you +want to - but the user-facing code is pretty much taken care of. + +=head2 METHODS + +The following methods are available from this class. + +=cut + +use base qw/WebGUI::Shop::PayDriver/; + +Readonly my $I18N => 'PayDriver_CreditCard'; + +#------------------------------------------------------------------- +sub _monthYear { + my $session = shift; + my $form = $session->form; + + tie my %months, "Tie::IxHash"; + tie my %years, "Tie::IxHash"; + %months = map { sprintf( '%02d', $_ ) => sprintf( '%02d', $_ ) } 1 .. 12; + %years = map { $_ => $_ } 2004 .. 2099; + + my $monthYear = + WebGUI::Form::selectBox( $session, { + name => 'expMonth', + options => \%months, + value => [ $form->process("expMonth") ] + }) + . " / " + . WebGUI::Form::selectBox( $session, { + name => 'expYear', + options => \%years, + value => [ $form->process("expYear") ] + }); + + return $monthYear; +} + +#------------------------------------------------------------------- + +=head2 appendCredentialVars + +Add template vars for www_getCredentials. Override this to add extra fields. + +=cut + +sub appendCredentialVars { + my ($self, $var) = @_; + my $session = $self->session; + my $u = $session->user; + my $form = $session->form; + my $i18n = WebGUI::International->new($session, $I18N); + + $var->{formHeader} = WebGUI::Form::formHeader($session) + . $self->getDoFormTags('pay'); + + $var->{formFooter} = WebGUI::Form::formFooter(); + + my @fieldLoop; + + # Credit card information + $var->{cardNumberField} = WebGUI::Form::text($session, { + name => 'cardNumber', + value => $self->session->form->process("cardNumber"), + }); + $var->{monthYearField} = WebGUI::Form::readOnly($session, { + value => _monthYear( $session ), + }); + $var->{cvv2Field} = WebGUI::Form::integer($session, { + name => 'cvv2', + value => $self->session->form->process("cvv2"), + }) if $self->get('useCVV2'); + + $var->{checkoutButton} = WebGUI::Form::submit($session, { + value => $i18n->get('checkout button', 'Shop'), + }); + + return; +} + +#------------------------------------------------------------------- +sub definition { + my ($class, $session, $definition) = @_; + + my $i18n = WebGUI::International->new($session, $I18N); + + tie my %fields, 'Tie::IxHash', ( + useCVV2 => { + fieldType => 'yesNo', + label => $i18n->get('use cvv2'), + hoverHelp => $i18n->get('use cvv2 help'), + }, + credentialsTemplateId => { + fieldType => 'template', + label => $i18n->get('credentials template'), + hoverHelp => $i18n->get('credentials template help'), + namespace => 'Shop/Credentials', + defaultValue => 'itransact_credentials1', + }, + ); + + push @{ $definition }, { + name => 'Credit Card Base Class', + properties => \%fields, + }; + + return $class->SUPER::definition($session, $definition); +} + +#------------------------------------------------------------------- + +=head2 processCredentials + +Process the form where credentials (name, address, phone number and credit card information) +are entered. + +=cut + +sub processCredentials { + my $self = shift; + my $session = $self->session; + my $form = $session->form; + my $i18n = WebGUI::International->new($session, $I18N); + my @error; + + # Check credit card data + push @error, $i18n->get( 'invalid card number' ) unless $form->integer('cardNumber'); + push @error, $i18n->get( 'invalid cvv2' ) if ($self->get('useCVV2') && !$form->integer('cvv2')); + + # Check if expDate and expYear have sane values + my ($currentYear, $currentMonth) = $self->session->datetime->localtime; + my $expires = $form->integer( 'expYear' ) . sprintf '%02d', $form->integer( 'expMonth' ); + my $now = $currentYear . sprintf '%02d', $currentMonth; + + push @error, $i18n->get('invalid expiration date') unless $expires =~ m{^\d{6}$}; + push @error, $i18n->get('expired expiration date') unless $expires >= $now; + + return \@error if scalar @error; + # Everything ok process the actual data + $self->{ _cardData } = { + acct => $form->integer( 'cardNumber' ), + expMonth => $form->integer( 'expMonth' ), + expYear => $form->integer( 'expYear' ), + cvv2 => $form->integer( 'cvv2' ), + }; + + return; +} + +#------------------------------------------------------------------- + +=head2 www_getCredentials ( $errors ) + +Build a templated form for asking the user for their credentials. + +=head3 $errors + +An array reference of errors to show the user. + +=cut + +sub www_getCredentials { + my $self = shift; + my $errors = shift; + my $session = $self->session; + my $form = $session->form; + my $i18n = WebGUI::International->new($session, $I18N); + my $var = {}; + +# Process form errors + $var->{errors} = []; + if ($errors) { + $var->{error_message} = $i18n->get('error occurred message'); + foreach my $error (@{ $errors} ) { + push @{ $var->{errors} }, { error => $error }; + } + } + + $self->appendCredentialVars($var); + $self->appendCartVariables($var); + + my $template = WebGUI::Asset::Template->new($session, $self->get("credentialsTemplateId")); + my $output; + if (defined $template) { + $template->prepare; + $output = $template->process($var); + } + else { + $output = $i18n->get('template gone'); + } + + return $session->style->userStyle($output); +} + +#------------------------------------------------------------------- + +=head2 www_pay + +Makes sure that the user has all the requirements for checking out, including +getting credentials, it processes the transaction and then displays a thank +you screen. + +=cut + +sub www_pay { + my $self = shift; + my $session = $self->session; + # Check whether the user filled in the checkout form and process those. + my $credentialsErrors = $self->processCredentials; + + # Go back to checkout form if credentials are not ok + return $self->www_getCredentials( $credentialsErrors ) if $credentialsErrors; + + # Payment time! + my $transaction = $self->processTransaction( ); + + if ($transaction->get('isSuccessful')) { + return $transaction->thankYou(); + } + + # Payment has failed... + return $self->displayPaymentError($transaction); +} + +1; + diff --git a/lib/WebGUI/Shop/PayDriver/CreditCard/AuthorizeNet.pm b/lib/WebGUI/Shop/PayDriver/CreditCard/AuthorizeNet.pm new file mode 100644 index 000000000..dbeb31dee --- /dev/null +++ b/lib/WebGUI/Shop/PayDriver/CreditCard/AuthorizeNet.pm @@ -0,0 +1,267 @@ +package WebGUI::Shop::PayDriver::CreditCard::AuthorizeNet; + +use strict; + +use base qw/WebGUI::Shop::PayDriver::CreditCard/; + +use DateTime; +use Readonly; +use Business::OnlinePayment; + +Readonly my $I18N => 'PayDriver_AuthorizeNet'; + +=head1 NAME + +WebGUI::Shop::PayDriver::CreditCard::AuthorizeNet + +=head1 DESCRIPTION + +Payment driver that uses Business::OnlinePayment to process transactions +through Authorize.net + +=head1 SYNOPSIS + + # in webgui config file... + + "paymentDrivers" : [ + "WebGUI::Shop::PayDriver::Cash", + "WebGUI::Shop::PayDriver::CreditCard::AuthorizeNet", + ... + ], + +=head1 METHODS + +The following methods are available from this class: + +=cut + +#------------------------------------------------------------------- + +=head2 appendCredentialVars ( var ) + +Overridden to add the card type field + +=cut + +sub appendCredentialVars { + my ( $self, $var ) = @_; + my $session = $self->session; + my $i18n = WebGUI::International->new( $session, $I18N ); + $self->SUPER::appendCredentialVars($var); + + $var->{cardTypeField} = WebGUI::Form::selectBox( + $session, { + name => 'cardType', + options => { map { $_ => $_ } ( 'Visa', 'MasterCard', 'American Express', 'Discover', ) }, + } + ); + + return; +} ## end sub appendCredentialVars + +#------------------------------------------------------------------- + +=head2 cancelRecurringPayment ( transaction ) + +Cancels a recurring transaction. Returns an array containing ( isSuccess, gatewayStatus, gatewayError). + +=head3 transaction + +The instanciated recurring transaction object. + +=cut + +sub cancelRecurringPayment { + my ( $self, $transaction ) = @_; + my $session = $self->session; + + my $tx = $self->gatewayObject; + $tx->content( + subscription => $transaction->get('transactionCode'), + login => $self->get('login'), + password => $self->get('transaction_key'), + action => 'Cancel Recurring Authorization', + ); + $tx->submit; + + return $self->gatewayResponse($tx); +} + +#------------------------------------------------------------------- +sub definition { + my ( $class, $session, $definition ) = @_; + + my $i18n = WebGUI::International->new( $session, $I18N ); + + tie my %fields, 'Tie::IxHash', ( + login => { + fieldType => 'text', + label => $i18n->get('login'), + hoverHelp => $i18n->get('login help'), + }, + transaction_key => { + fieldType => 'text', + label => $i18n->get('transaction key'), + hoverHelp => $i18n->get('transaction key help'), + }, + testMode => { + fieldType => 'YesNo', + label => $i18n->get('test mode'), + hoverHelp => $i18n->get('test mode help'), + }, + ); + + push @{$definition}, { + name => $i18n->get('name'), + properties => \%fields, + }; + + return $class->SUPER::definition( $session, $definition ); +} ## end sub definition + +#------------------------------------------------------------------- + +=head2 gatewayObject ( params ) + +Returns a Business::OnlinePayment object, possibly with options set from the +paydriver properties. params can be a hashref of the options that would +normally be passed to tx->content, in which case these will be passed along. + +=cut + +sub gatewayObject { + my ( $self, $params ) = @_; + + my $tx = Business::OnlinePayment->new('AuthorizeNet'); + if ( $self->get('testMode') ) { + + # Yes, we need to do both these things. The BOP interfaces tend to + # ony honor one or the other of them. + $tx->test_transaction(1); + $tx->server('test.authorize.net'); + } + $tx->content(%$params) if $params; + + return $tx; +} + +#------------------------------------------------------------------- + +=head2 gatewayResponse ( tx ) + +Returns the various responses required by the PayDriver interface from the +passed Business::OnlinePayment object. + +=cut + +sub gatewayResponse { + my ( $self, $tx ) = @_; + return ( $tx->is_success, $tx->order_number, $tx->result_code, $tx->error_message ); +} + +#------------------------------------------------------------------- + +sub handlesRecurring {1} + +#------------------------------------------------------------------- + +=head2 paymentParams + +Returns a hashref of the billing address and card information, translated into +a form that Business::OnlinePayment likes + +=cut + +sub paymentParams { + my $self = shift; + my $card = $self->{_cardData}; + my $bill = $self->getCart->getBillingAddress->get(); + + my %params = ( + type => $card->{type}, + login => $self->get('login'), + transaction_key => $self->get('transaction_key'), + first_name => $bill->{firstName}, + last_name => $bill->{lastName}, + address => $bill->{address1}, + city => $bill->{city}, + state => $bill->{state}, + zip => $bill->{code}, + card_number => $card->{acct}, + expiration => sprintf '%2d/%2d', + @{$card}{ 'expMonth', 'expYear' }, + ); + $params{cvv2} = $card->{cvv2} if $self->get('useCVV2'); + return \%params; +} ## end sub paymentParams + +#------------------------------------------------------------------- + +sub processCredentials { + my $self = shift; + my $session = $self->session; + my $i18n = WebGUI::International->new( $session, $I18N ); + my $error = $self->SUPER::processCredentials; + + my $type = $session->form->process('cardType'); + + unless ($type) { + $error ||= []; + push @$error, $i18n->get('invalid cardType'); + } + + return $error if defined $error; + + $self->{_cardData}->{type} = $type; + + return; +} ## end sub processCredentials + +#------------------------------------------------------------------- + +sub processPayment { + my ( $self, $transaction ) = @_; + my $params = $self->paymentParams; + + if ( $transaction->isRecurring ) { + my $items = $transaction->getItems; + if ( @$items > 1 ) { + WebGUI::Error::InvalidParam->throw( + error => 'This payment gateway can only handle one recurring item at a time' ); + } + + my $item = $items->[0]; + my $sku = $item->getSku; + + my %translateInterval = ( + Weekly => '7 days', + BiWeekly => '14 days', + FourWeekly => '28 days', + Monthly => '1 month', + Quarterly => '3 months', + HalfYearly => '6 months', + Yearly => '12 months', + ); + + # BOP::AuthorizeNet::ARB has an API that's inconsistant with the AIM + # api -- it wants password instead of transaction_key. Go figure. + $params->{password} = delete $params->{transaction_key}; + + $params->{action} = 'Recurring Authorization'; + $params->{interval} = $translateInterval{ $sku->getRecurInterval }; + $params->{start} = DateTime->today->ymd; + $params->{periods} = '9999'; # magic value that means 'never stop' + $params->{description} = $item->get('configuredTitle'); + } ## end if ( $transaction->isRecurring) + else { + $params->{action} = 'Normal Authorization'; + } + + $params->{amount} = $transaction->get('amount'); + my $tx = $self->gatewayObject($params); + $tx->submit; + return $self->gatewayResponse($tx); +} ## end sub processPayment + +1; + diff --git a/lib/WebGUI/i18n/English/PayDriver_AuthorizeNet.pm b/lib/WebGUI/i18n/English/PayDriver_AuthorizeNet.pm new file mode 100644 index 000000000..312c19781 --- /dev/null +++ b/lib/WebGUI/i18n/English/PayDriver_AuthorizeNet.pm @@ -0,0 +1,50 @@ +package WebGUI::i18n::English::PayDriver_AuthorizeNet; + +use strict; + +our $I18N = { + 'cardType' => { + message => q{Card Type}, + lastUpdated => 1101772177, + context => q{Form label in the checkout form of the AuthorizeNet module.}, + }, + 'login' => { + message => q{API Login}, + lastUpdated => 1247613128, + context => q{Form label in the configuration form of the AuthorizeNet module.}, + }, + 'login help' => { + message => q{The API login id for your Authorize.net account}, + lastUpdated => 1247613146, + context => q{Hover help for the login field of the AuthorizeNet module}, + }, + 'name' => { + message => q{Credit Card (Authorize.net)}, + lastUpdated => 0, + context => q{Name of the Authorize.net module}, + }, + 'test mode' => { + message => q{Test Mode}, + lastUpdated => 0, + context => q{Form label for test mode toggle in AuthroizeNet module}, + }, + 'test mode help' => { + message => q{Whether calls using this gateway should be made in test mode}, + lastUpdated => 0, + context => q{Hover help for test mode form field}, + }, + 'transaction key' => { + message => q{Transaction Key}, + lastUpdated => 1247613060, + context => q{Form label in the configuration form of the AuthorizeNet module.}, + }, + 'transaction key help' => { + message => q{The Transaction Key for your Authorize.net account}, + lastUpdated => 1247613119, + context => q{Hover help for the password field of the AuthorizeNet module}, + }, +}; + +1; + +#vim:ft=perl diff --git a/lib/WebGUI/i18n/English/PayDriver_CreditCard.pm b/lib/WebGUI/i18n/English/PayDriver_CreditCard.pm new file mode 100644 index 000000000..ba38c60f4 --- /dev/null +++ b/lib/WebGUI/i18n/English/PayDriver_CreditCard.pm @@ -0,0 +1,161 @@ +package WebGUI::i18n::English::PayDriver_CreditCard; +use strict; + +our $I18N = { + 'cardNumber' => { + message => q|Credit card number|, + lastUpdated => 1101772177, + context => q|Form label in the checkout form of the Credit Card module.| + }, + 'credentials template' => { + message => q|Credentials Template|, + lastUpdated => 0, + context => q|Form label in the configuration form of the Credit Card module.| + }, + 'credentials template help' => { + message => q|Pick a template to display the form where the user will enter in their billing information and credit card information.|, + lastUpdated => 0, + context => q|Hover help for the credentials template field in the configuration form of the Credit Card module.| + }, + 'cvv2' => { + message => q|Verification number (ie. CVV2)|, + lastUpdated => 1101772182, + context => q|Form label in the checkout form of the Credit Card module.| + }, + 'error occurred message' => { + message => q|The following errors occurred:|, + lastUpdated => 0, + context => q|The message that tell the user that there were some errors in their submitted credentials.|, + }, + 'expiration date' => { + message => q|Expiration date|, + lastUpdated => 1101772180, + context => q|Form label in the checkout form of the Credit Card module.| + }, + 'expired expiration date' => { + message => q|The expiration date on your card has already passed.|, + lastUpdated => 0, + context => q|An error indicating that an an expired card was used.| + }, + 'invalid firstName' => { + message => q|You have to enter a valid first name.|, + lastUpdated => 0, + context => q|An error indicating that an invalid first name has been entered.| + }, + 'invalid lastName' => { + message => q|You have to enter a valid last name.|, + lastUpdated => 0, + context => q|An error indicating that an invalid last name has been entered.| + }, + 'invalid address' => { + message => q|You have to enter a valid address.|, + lastUpdated => 0, + context => q|An error indicating that an invalid street has been entered.| + }, + 'invalid city' => { + message => q|You have to enter a valid city.|, + lastUpdated => 0, + context => q|An error indicating that an invalid city has been entered.| + }, + 'invalid zip' => { + message => q|You have to enter a valid zipcode.|, + lastUpdated => 0, + context => q|An error indicating that an invalid zipcode has been entered.| + }, + 'invalid email' => { + message => q|You have to enter a valid email address.|, + lastUpdated => 0, + context => q|An error indicating that an invalid email address has been entered.| + }, + 'invalid card number' => { + message => q|You have to enter a valid credit card number.|, + lastUpdated => 0, + context => q|An error indicating that an invalid credit card number has been entered.| + }, + 'invalid cvv2' => { + message => q|You have to enter a valid card security code (ie. cvv2).|, + lastUpdated => 0, + context => q|An error indicating that an invalid card security code has been entered.| + }, + 'invalid expiration date' => { + message => q|You have to enter a valid expiration date.|, + lastUpdated => 0, + context => q|An error indicating that an invalid expiration date has been entered.| + }, + 'template gone' => { + message => q|The template for entering in credentials has been deleted. Please notify the site administrator.|, + lastUpdated => 0, + context => q|Error message when the getCredentials template cannot be accessed.| + }, + 'use cvv2' => { + message => q|Use CVV2|, + lastUpdated => 0, + context => q|Form label in the configuration form of the Credit Card module.| + }, + 'use cvv2 help' => { + message => q|Set this option to yes if you want to use CVV2.|, + lastUpdated => 0, + context => q|Form label in the configuration form of the Credit Card module.| + }, + + 'edit credentials template' => { + message => q|Edit Credentials Template|, + lastUpdated => 0, + context => q|Title of the help page.| + }, + 'edit credentials template help' => { + message => q|This template is used to display a form to the user where they can enter in contact and credit card billing information.|, + lastUpdated => 0, + context => q|Title of the help page.| + }, + + 'errors help' => { + message => q{A template loop containing a list of errors from processing the form.}, + lastUpdated => 0, + context => q{Template variable help.}, + }, + 'error help' => { + message => q{One error from the errors loop. It will have minimal markup.}, + lastUpdated => 0, + context => q{Template variable help.}, + }, + 'addressField help' => { + message => q{A single text field for the user to enter in their street address.}, + lastUpdated => 0, + context => q{Template variable help.}, + }, + 'emailField help' => { + message => q{A single text field for the user to enter in their email address.}, + lastUpdated => 1231192368, + context => q{Template variable help.}, + }, + 'cardNumberField help' => { + message => q{A single text field for the user to enter in their credit card number.}, + lastUpdated => 0, + context => q{Template variable help.}, + }, + 'monthYearField help' => { + message => q{A combination form field for the user to enter in the month and year of the expiration date for the credit card.}, + lastUpdated => 0, + context => q{Template variable help.}, + }, + 'cvv2Field help' => { + message => q{A single text field for the user to enter in their credit card verification number. If the PayDriver is not configured to use CVV2, then this field will be empty.}, + lastUpdated => 0, + context => q{Template variable help.}, + }, + 'checkoutButton help' => { + message => q{A button with an internationalized label to submit the form and continue the checkout process.}, + lastUpdated => 0, + context => q{Template variable help.}, + }, + + 'fields help' => { + message => q{A loop of all the available fields for convenience. Each +entry in the loop contains name (field name), label (an internationalized +label for the field), and field (the same as in stateField, cityField, etc).}, + lastUpdated => 0, + }, +}; + +1; diff --git a/sbin/testEnvironment.pl b/sbin/testEnvironment.pl index 68cfec516..1e7410c10 100755 --- a/sbin/testEnvironment.pl +++ b/sbin/testEnvironment.pl @@ -144,6 +144,8 @@ checkModule("CSS::Minifier::XS", "0.03" ); checkModule("JavaScript::Minifier::XS", "0.05" ); checkModule("Readonly", "1.03" ); checkModule("Business::PayPal::API", "0.62" ); +checkModule("Business::OnlinePayment", "3.01" ); +checkModule("Business::OnlinePayment::AuthorizeNet", "3.21" ); checkModule("Locales", "0.10" ); checkModule("Test::Harness", "3.17" ); checkModule("DateTime::Event::ICal", "0.10" );