diff --git a/lib/WebGUI/Asset/Sku.pm b/lib/WebGUI/Asset/Sku.pm index 1cc20d85a..80a6fa3ac 100644 --- a/lib/WebGUI/Asset/Sku.pm +++ b/lib/WebGUI/Asset/Sku.pm @@ -253,6 +253,19 @@ sub getQuantityAvailable { #------------------------------------------------------------------- +=head2 getRecurInterval ( ) + +Returns the recur interval, which must be one of the following: 'Weekly', 'BiWeekly', 'FourWeekly', +'Monthly', 'Quarterly', 'HalfYearly' or 'Yearly'. Must be overriden by subclass if that is a recurring Sku. + +=cut + +sub getRecurInterval { + return undef; +} + +#------------------------------------------------------------------- + =head2 getTaxRate ( ) Returns undef unless the "Override tax rate?" switch is set to yes. If it is, then it returns the value of the "Tax Rate Override" field. diff --git a/lib/WebGUI/Shop/PayDriver.pm b/lib/WebGUI/Shop/PayDriver.pm index 12ded5667..e3b3d2b26 100644 --- a/lib/WebGUI/Shop/PayDriver.pm +++ b/lib/WebGUI/Shop/PayDriver.pm @@ -496,7 +496,8 @@ sub processTransaction { my $transaction = WebGUI::Shop::Transaction->create($self->session,{ paymentMethod => $self, paymentAddress => $paymentAddress, - }); + cart => $cart, + }); my ($success, $transactionCode, $statusCode, $statusMessage) = $self->processPayment; if ($success) { $transaction->completePurchase($cart, $transactionCode, $statusCode, $statusMessage); diff --git a/lib/WebGUI/Shop/PayDriver/ITransact.pm b/lib/WebGUI/Shop/PayDriver/ITransact.pm new file mode 100644 index 000000000..342e6a9fd --- /dev/null +++ b/lib/WebGUI/Shop/PayDriver/ITransact.pm @@ -0,0 +1,404 @@ +package WebGUI::Shop::PayDriver::ITransact; + +use strict; +use XML::Simple; + +use base qw/WebGUI::Shop::PayDriver/; + +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; +} + +#------------------------------------------------------------------- +sub cancelRecurringPayment { + +} + + +#------------------------------------------------------------------- +sub definition { + my $class = shift; + my $session = shift; + my $definition = shift; + + my $i18n = WebGUI::International->new($session, 'PayDriver_Cash'); + + tie my %fields, 'Tie::IxHash'; + %fields = ( + vendorId => { + fieldType => 'text', + label => $i18n->echo('vendorId'), + hoverHelp => $i18n->echo('vendorId help'), + }, + password => { + fieldType => 'password', + label => $i18n->echo('password'), + hoverHelp => $i18n->echo('password help'), + }, + useCVV2 => { + fieldType => 'yesNo', + label => $i18n->echo('use cvv2'), + hoverHelp => $i18n->echo('use cvv2 help'), + }, + emailMessage => { + fieldType => 'textarea', + label => $i18n->echo('emailMessage'), + hoverHelp => $i18n->echo('emailMessage help'), + }, + # readonly stuff from old plugin here? + ); + + push @{ $definition }, { + name => $i18n->echo('Cash'), + properties => \%fields, + }; + + return $class->SUPER::definition($session, $definition); +} + +#------------------------------------------------------------------- +sub _generatePaymentRequestXML { + my $self = shift; + my $transaction = shift; + my $session = $self->session; + my $paymentAddress = $self->{ _paymentAddress }; + my $cardData = $self->{ _cardData }; + + # Set up the XML. + # --- Customer data part --- + my $billingAddress; + $billingAddress->{ Address1 } = $paymentAddress->{ address1 }; +# $billingAddress->{ Address2 } = $paymentAddress->{ address2 }; +# $billingAddress->{ Address3 } = $paymentAddress->{ address3 }; + $billingAddress->{ FirstName } = $paymentAddress->{ firstName }; + $billingAddress->{ LastName } = $paymentAddress->{ lastName }; + $billingAddress->{ City } = $paymentAddress->{ city }; + $billingAddress->{ State } = $paymentAddress->{ state }; + $billingAddress->{ Zip } = $paymentAddress->{ code }; + $billingAddress->{ Country } = $paymentAddress->{ country }; + $billingAddress->{ Phone } = $paymentAddress->{ phoneNumber }; + + my $cardInfo; + $cardInfo->{ CCNum } = $cardData->{ acct }; + $cardInfo->{ CCMo } = $cardData->{ expMonth }; + $cardInfo->{ CCYear } = $cardData->{ expYear }; + $cardInfo->{ CVV2Number } = $cardData->{ cvv2 } if $self->get('useCVV2'); + + my $customerData; + $customerData->{ Email } = $session->user->profileField('email'); + $customerData->{ BillingAddress } = $billingAddress; + $customerData->{ AccountInfo }->{ CardInfo } = $cardInfo; + + # --- Transaction data part --- + my $emailText; + $emailText->{ EmailTextItem } = [ + $self->get('emailMessage'), + 'ID: '. $transaction->getId, + ]; + + # Process items + my ($orderItems, $recurringData); + my $items = $transaction->getItems; + + # Check if recurring payments have a unique transaction + #### TODO: Throw the correct Exception Class + WebGUI::Error::InvalidParam->throw( error => 'Recurring transaction mixed with other transactions' ) + if ( (scalar @{ $items } > 1) && (grep { $_->get('isRecurring') } @{ $items }) ); + + foreach my $item (@{ $transaction->getItems }) { + ####TODO: How to handle intial payment? + if ( $item->get('isRecurring') ) { + $recurringData->{ RecurRecipe } = $self->resolveRecurRecipe( $item->get('recurInterval') ); + $recurringData->{ RecurReps } = 99999; + $recurringData->{ RecurTotal } = $item->getPrice; + $recurringData->{ RecurDesc } = $item->get('title'); + } + + push @{ $orderItems->{ Item } }, { + Description => $item->get('title'), + Cost => $item->getPrice, + Qty => $item->get('quantity'), + } + } + + my $vendorData; + $vendorData->{ Element }->{ Name } = 'transactionId'; + $vendorData->{ Element }->{ Value } = $transaction->getId; + + my $transactionData; + $transactionData->{ VendorId } = $self->get('vendorId'); + $transactionData->{ VendorPassword } = $self->get('password'); + $transactionData->{ VendorData } = $vendorData; + $transactionData->{ HomePage } = $self->session->setting->get("companyURL"); + $transactionData->{ RecurringData } = $recurringData if $recurringData; + $transactionData->{ EmailText } = $emailText if $emailText; + $transactionData->{ OrderItems } = $orderItems; + + # --- The XML structure --- + my $xmlStructure = { + SaleRequest => { + CustomerData => $customerData, + TransactionData => $transactionData, + } + }; + + my $xml = XMLout( $xmlStructure ); +} + +#------------------------------------------------------------------- +sub processCredentials { + my $self = shift; + my $session = $self->session; + my $form = $session->form; + my $i18n = WebGUI::International->new($session,'CommercePaymentITransact'); + my @error; + + # Check address data + push @error, $i18n->get( 'invalid firstName' ) unless $form->process( 'firstName' ); + push @error, $i18n->get( 'invalid lastName' ) unless $form->process( 'lastName' ); + push @error, $i18n->get( 'invalid address' ) unless $form->process( 'address' ); + push @error, $i18n->get( 'invalid city' ) unless $form->process( 'city' ); + push @error, $i18n->get( 'invalid email' ) unless $form->email ( 'email' ); + push @error, $i18n->get( 'invalid zip' ) + if ( !$form->zipcode( 'zipcode' ) && $form->process( 'country' ) eq 'United States' ); + + # 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; + + # Everything ok process the actual data + unless (@error) { + $self->{ _cardData } = { + acct => $form->integer( 'cardNumber' ), + expMonth => $form->integer( 'expMonth' ), + expYear => $form->integer( 'expYear' ), + cvv2 => $form->integer( 'cvv2' ), + }; + + $self->{ _billingAddress } = { + address1 => $form->process( 'address' ), + code => $form->zipcode( 'zipcode' ), + city => $form->process( 'city' ), + firstName => $form->process( 'firstName' ), + lastName => $form->process( 'lastName' ), + email => $form->email ( 'email' ), + state => $form->process( 'state' ), + country => $form->process( 'country' ), + phoneNumber => $form->process( 'phone' ), + }; + + return; + } + + return \@error; +} + + +#------------------------------------------------------------------- +sub processPayment { + my $self = shift; + my $transaction = shift; + my $session = $self->session; + + # Get the payment definition XML + my $xml = $self->_generatePaymentRequestXML( $transaction ); + + # Set up LWP + my $userAgent = LWP::UserAgent->new; + $userAgent->env_proxy; + $userAgent->agent("WebGUI "); + + # Create a request and stuff the xml in it + my $xmlTransactionScript = 'https://secure.paymentclearing.com/cgi-bin/rc/xmltrans.cgi'; + my $request = HTTP::Request->new( POST => $xmlTransactionScript ); + $request->content_type( 'application/x-www-form-urlencoded' ); + $request->content( 'xml='.$xml ); + + # Do the request + my $response = $userAgent->request($request); + + # Process response + if ($response->is_success) { + # We got some XML back from iTransact, now parse it. + my $transactionResult = XMLin( $response->content ); +#### TODO: More checking: price, address, etc + unless (defined $transactionResult->{ TransactionData }) { + # GatewayFailureResponse: This means the xml is invalid or has the wrong mime type + return( + 0, + undef, + $transactionResult->{ Status }, + $transactionResult->{ ErrorMessage } . ' Category: ' . $transactionResult->{ ErrorCategory } + ); + } else { + # SaleResponse: We have succesfully sent the XML and it was correct. Note that this doesn't mean that + # the transaction has succeeded. It only has if Status is set to OK. + my $transactionData = $transactionResult->{ TransactionData }; + + my $status = $transactionData->{ Status }; + my $errorMessage = $transactionData->{ ErrorMessage }; + my $errorCategory = $transactionData->{ ErrorCategory }; + my $gatewayCode = $transactionData->{ XID }; + my $isSuccess = $status eq 'OK'; + + return ( $isSuccess, $gatewayCode, $status, "$errorMessage Category: $errorCategory" ); + } + } else { + # Connection Error + + return ( 0, undef, 'ConnectionError', $response->status_line ); + } +} + +#------------------------------------------------------------------- +sub www_confirmRecurringTransaction { + my $self = shift; + my $session = $self->session; + my $form = $session->form; + + # Fetch transaction + my $gatewayId = $form->process( 'orig_xid' ); +# Somehow, the lines below aren't used for nothing, but were in the original code... +# my $transaction = WebGUI::Shop::Transaction->getByGatewayTransactionId( $session, $gatewayId, $self ); +# my $itemProperties = $transaction->getItems->[0]; + + # Convert the passed timestamps to epochs + my $startEpoch = $session->datetime->setToEpoch(sprintf("%4d-%02d-%02d %02d:%02d:%02d", unpack('a4a2a2a2a2a2', $form->process("start_date")))); + my $currentEpoch = $session->datetime->setToEpoch(sprintf("%4d-%02d-%02d %02d:%02d:%02d", unpack('a4a2a2a2a2a2', $form->process("when")))); + + # Update record + $session->db->setRow( 'ITransact_recurringStatus', 'gatewayId', { + gatewayId => $gatewayId, + initDate => $startEpoch, + lastTransaction => $currentEpoch, + status => $form->process( 'status' ), + errorMessage => $form->process( 'error_message' ), + recipe => $form->process( 'recipe_name' ), + }); +} + +#------------------------------------------------------------------- +sub www_getCredentials { + my $self = shift; + my $session = $self->session; + my $form = $session->form; + my $i18n = WebGUI::International->new($self->session, 'CommercePaymentITransact'); + my $u = WebGUI::User->new($self->session,$self->session->user->userId); + + my $f = WebGUI::HTMLForm->new( $session ); + $self->getDoFormTags( 'pay', $f ); + + # Address data form + $f->text( + -name => 'firstName', + -label => $i18n->get('firstName'), + -value => $form->process("firstName") || $u->profileField('firstName'), + ); + $f->text( + -name => 'lastName', + -label => $i18n->get('lastName'), + -value => $form->process("lastName") || $u->profileField('lastName'), + ); + $f->text( + -name => 'address', + -label => $i18n->get('address'), + -value => $form->process("address") || $u->profileField('homeAddress'), + ); + $f->text( + -name => 'city', + -label => $i18n->get('city'), + -value => $form->process("city") || $u->profileField('homeCity'), + ); + $f->text( + -name => 'state', + -label => $i18n->get('state'), + -value => $form->process("state") || $u->profileField('homeState'), + ); + $f->zipcode( + -name => 'zipcode', + -label => $i18n->get('zipcode'), + -value => $form->process("zipcode") || $u->profileField('homeZip'), + ); + $f->country( + -name => "country", + -label => $i18n->get("country"), + -value => ($form->process("country",'country') || $u->profileField("homeCountry") || 'United States'), + ); + $f->phone( + -name => "phone", + -label => $i18n->get("phone"), + -value => $form->process("phone",'phone') || $u->profileField("homePhone"), + ); + $f->email( + -name => 'email', + -label => $i18n->get('email'), + -value => $self->session->form->process("email") || $u->profileField('email'), + ); + + # Credit card information + $f->text( + -name => 'cardNumber', + -label => $i18n->get('cardNumber'), + -value => $self->session->form->process("cardNumber"), + ); + $f->readOnly( + -label => $i18n->get('expiration date'), + -value => monthYear( $session ), + ); + $f->integer( + -name => 'cvv2', + -label => $i18n->get('cvv2'), + -value => $self->session->form->process("cvv2") + ) if ($self->get('useCVV2')); + + return $f->print; +} + +#------------------------------------------------------------------- +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! + $self->processTransaction; + + return $session->style->userStyle('Thank you for your order'); +} + +1; + diff --git a/lib/WebGUI/Shop/TransactionItem.pm b/lib/WebGUI/Shop/TransactionItem.pm index e8dc9df28..6bc14d2a9 100644 --- a/lib/WebGUI/Shop/TransactionItem.pm +++ b/lib/WebGUI/Shop/TransactionItem.pm @@ -215,24 +215,27 @@ sub update { my ($self, $newProperties) = @_; my $id = id $self; if (exists $newProperties->{item}) { - my $item = $newProperties->{item}; + my $item = $newProperties->{ item }; my $sku = $item->getSku; - $newProperties->{options} = $sku->getOptions; - $newProperties->{assetId} = $sku->getId; - $newProperties->{price} = $sku->getPrice; - $newProperties->{configuredTitle} = $sku->getConfiguredTitle; + $newProperties->{ options } = $sku->getOptions; + $newProperties->{ assetId } = $sku->getId; + $newProperties->{ price } = $sku->getPrice; + $newProperties->{ configuredTitle } = $sku->getConfiguredTitle; + $newProperties->{ isRecurring } = $sku->isRecurring; + $newProperties->{ recurInterval } = $sku->getRecurInterval if $sku->isRecurring; + my $address = $item->getShippingAddress; - $newProperties->{shippingAddressId} = $address->getId; - $newProperties->{shippingAddressName} = $address->get('name'); - $newProperties->{shippingAddress1} = $address->get('address1'); - $newProperties->{shippingAddress2} = $address->get('address2'); - $newProperties->{shippingAddress3} = $address->get('address3'); - $newProperties->{shippingCity} = $address->get('city'); - $newProperties->{shippingState} = $address->get('state'); - $newProperties->{shippingCountry} = $address->get('country'); - $newProperties->{shippingCode} = $address->get('code'); - $newProperties->{shippingPhoneNumber} = $address->get('phoneNumber'); - $newProperties->{quantity} = $item->get('quantity'); + $newProperties->{ shippingAddressId } = $address->getId; + $newProperties->{ shippingAddressName } = $address->get('name'); + $newProperties->{ shippingAddress1 } = $address->get('address1'); + $newProperties->{ shippingAddress2 } = $address->get('address2'); + $newProperties->{ shippingAddress3 } = $address->get('address3'); + $newProperties->{ shippingCity } = $address->get('city'); + $newProperties->{ shippingState } = $address->get('state'); + $newProperties->{ shippingCountry } = $address->get('country'); + $newProperties->{ shippingCode } = $address->get('code'); + $newProperties->{ shippingPhoneNumber } = $address->get('phoneNumber'); + $newProperties->{ quantity } = $item->get('quantity'); } my @fields = (qw(assetId configuredTitle options shippingAddressId shippingTrackingNumber shippingStatus shippingName shippingAddress1 shippingAddress2 shippingAddress3 shippingCity shippingState