#!/usr/local/bin/perl # payment.pl - web cgi routine to process payments during checkout # # Dave Stoddard - 6/02/04 # dgs@accelix.com # # Copyright # --------- # Copyright(c) 2004, Accelix LLC. All Rights Reserved. # # Description # ----------- # This program handles payment processing for the shopping cart system. If # the payment is accepted, an email message is sent to the customer and an # invoice is displayed with all of the pertinent data. If the payment is # declined by the credit card company, the customer is given a choice to use # another credit card. This program is the last in a series of programs that # handle the check out process. The steps in the checkout process, and their # associated modules, are as follows: # # checkout.pl - collect shipping information for the customer's order # billing.pl - show total and collect billing information # confirm.pl - show total and confirm or cancel the order # payment.pl - process payment and provide an invoice # # Note: This routine expects that the Authorize.Net session is configured # to use "&" as a delimited field separator using Direct Response with version # 3.1 of the software. You must go into "settings" on the terminal and # make sure version 3.1 is used for transactions, and that the Delimited # Response option is enabled in the Direct Response settings menu. # # RCS Information # --------------- # $Id$ # $Log$ # # package main; use strict 'vars'; use strict 'subs'; # use warnings; # use diagnostics; # load application modules use cart; # shopping cart support routines use Time::Local; # perl time functions use Net::SSLeay qw(post_https make_headers make_form); # SSL interface # --------------------------- # DECLARATIONS AND PROTOTYPES # --------------------------- # local elements my %form = (); # data from form (name/value pairs) my %state = (); # state persistence data hash my %sess = (); # session id hash my %orders = (); # orders table record my %shipto = (); # shipto table record my %billto = (); # billto table record my %cart = (); # shopping cart record my %tran = (); # payment transaction result hash my @recs = (); # shopping cart records my $prev = "/cgi-bin/confirm.pl"; # previous CGI program my $curr = "/cgi-bin/payment.pl"; # this CGI program my $itemtot = 0; # item total my $i = 0; # loop iteration element my $admin = "cart\@opencart.org"; # order management my $smfrom = "orders\@opencart.org"; # order from email address my $sendmail = "/usr/sbin/sendmail"; # path to sendmail program # authorize.net security info my $login = "userid"; # authorize.net userid my $passwd = "password"; # authorize.net password my $storename = "My Store"; # business name for credit card charges # my $testmode = "FALSE"; # set to FALSE in production my $testmode = "TRUE"; # set to TRUE in test mode # function prototypes sub ProcessCreditCard (); # process credit card payment sub UpdatePayment (\%\%); # update payment info in billto record sub CreateOrder (\%\@); # move cart record to order tables sub EmailInvoice (\%\%\%); # email an inbvoice to the customer sub DisplayInvoicePage (\%\%\%); # display an order invoice page sub DisplayPaymentDeclined (\%); # display payment declined page sub GetOrdersRecord ($); # obtain an orders record sub GetShiptoRecord ($); # obtain a shipto record sub GetBilltoRecord ($); # obtain a billto record sub AddOrderdRecord (\%); # add order detail records # initialize the program %form = ReadForm (); # read the form %state = GetState (%form); # get current state data (cookies) %sess = GetSession (%form,%state); # get session information # make sure the orders record exists %orders = GetOrdersRecord ($sess{sessid}); if (CheckError()) { DisplayError(); ExitProgram(); } unless (%orders) { ErrorMsg ("Order master record is missing -- checkout aborted."); DisplayError(); ExitProgram(); } # retrieve items from the shopping cart @recs = GetCartRecords ($sess{sessid}); if (CheckError()) { ErrorMsg ("Unable to read contents of shopping cart."); DisplayError(); ExitProgram(); } unless (@recs) { ErrorMsg ("There are no items in your shopping cart to checkout with."); DisplayError(); ExitProgram(); } # accumulate totals for the shopping cart foreach $i (@recs) { # convert the record into a hash %cart = DecodeCartRecord ($i); $itemtot = ($cart{price} * $cart{quantity}) + $itemtot; $itemtot = sprintf ("%4.2f",$itemtot); } # verify the total against the shopping cart if ($itemtot != $orders{itemtot}) { ErrorMsg ("Shopping cart total does not agree with orders record -- checkout aborted."); DisplayError(); ExitProgram(); } # make sure the shipto record exists %shipto = GetShiptoRecord ($sess{sessid}); if (CheckError()) { DisplayError(); ExitProgram(); } unless (%shipto) { ErrorMsg ("Shipping record is missing -- checkout aborted."); DisplayError(); ExitProgram(); } # make sure the billto record exists %billto = GetBilltoRecord ($sess{sessid}); if (CheckError()) { DisplayError(); ExitProgram(); } unless (%billto) { ErrorMsg ("Billing record is missing -- checkout aborted."); DisplayError(); ExitProgram(); } # make sure we use the amount from the order total if ($billto{due} != $orders{total}) { ErrorMsg ("Billto due of $billto{due} not equal to order amount of $orders{total}"); DisplayError(); ExitProgram(); } # process the credit card transaction %tran = ProcessCreditCard (); if ($tran{status} eq "1") { UpdatePayment (%tran,%billto); CreateOrder (%orders,@recs); $sess{sessid} = NewSessionID (); DisplayInvoicePage (%tran,%billto,%shipto); EmailInvoice (%tran,%billto,%shipto); } else { DisplayPaymentDeclined (%tran); } ExitProgram (); ### End Main Program ### ### ### ProcessCreditCard() will process credit card data. Based on the return ### code, the routine will either invoke a routine to show the card was ### accepted, or a routine to handle declines. ### ### This routine works for Authorize.Net ADC Direct Response only. ### sub ProcessCreditCard () { my %tran = (); # transaction result hash my @name = (); # name on card my @resp = (); # response table my %data = (); # credit card elements my %rhead = (); # HTTP headers on return my $rdata = ""; # transaction return data my $rtype = ""; # connection return code my $rc = 0; # numeric return code my $form = ""; # data string my $fname = ""; # credit card first name my $lname = ""; # credit card last name my $xstatus = ""; # return status code my $xsubcode = ""; # response internal subcode my $xreacode = ""; # reason code my $xreatext = ""; # reason text my $xauthcode = ""; # six-digit approval code my $xavscode = ""; # address verification codes my $xtransid = ""; # system transaction id my $xmd5hash = ""; # md5 hash returned my $xcvvresp = ""; # cvv2 response code (M = match) # connection info my $host = "secure.authorize.net"; # SSL connection host my $script = "/gateway/transact.dll"; # CGI to execute on host my $port = "443"; # SSL connection port number # constants my $ccmethod = "CC"; # payment method (CC or ECHECK) my $version = "3.1"; # ADC response version my $adcdelim = "TRUE"; # required for direct connect my $adcurl = "FALSE"; # required for direct connect my $authtype = "AUTH_CAPTURE"; # transaction type # determine the first and last names @name = split (/ /,$billto{name}); $fname = shift (@name); $lname = join (" ",@name); # standard transaction values $data{x_Login} = $login; $data{x_Password} = $passwd; $data{x_Version} = $version; $data{x_ADC_Delim_Data} = $adcdelim; $data{x_ADC_URL} = $adcurl; $data{x_Type} = $authtype; $data{x_Test_Request} = $testmode; $data{x_Method} = $ccmethod; $data{x_Description} = $storename; # form values $data{x_First_Name} = $fname if ($fname); $data{x_Last_Name} = $lname if ($lname); $data{x_Company} = $billto{company} if ($billto{company}); $data{x_Address} = $billto{address1} if ($billto{address1}); $data{x_Address} .= " $billto{address2}" if ($billto{address2}); $data{x_City} = $billto{city} if ($billto{city}); $data{x_State} = $billto{state} if ($billto{state}); $data{x_Country} = $billto{country} if ($billto{country}); $data{x_Zip} = $billto{zipcode} if ($billto{zipcode}); # credit card data $billto{ccexpmm} = sprintf ("%2.2d",$billto{ccexpmm}); $billto{ccexpyy} = sprintf ("%2.2d",$billto{ccexpyy}); $data{x_Card_Num} = $billto{ccnumber}; $data{x_Exp_Date} = "$billto{ccexpmm}$billto{ccexpyy}"; $data{x_Card_Code} = $billto{ccseccode} if ($billto{ccseccode}); $data{x_Amount} = $billto{due}; # create the data string and process the transaction $form = make_form (%data); ($rdata,$rtype,%rhead) = post_https ($host, $port, $script, "", $form); # determine if the connection worked if ($rtype =~ /HTTP\/1\.1 (\d+) /i) { $rc = $1; } unless ($rc eq "200") { ErrorMsg ("Connection to payment gateway failed: $rtype"); DisplayError (); ExitProgram (); } # parse the result data into a table @resp = split (/&/,$rdata); $tran{status} = $resp[0]; # status 1=success, 2=decline, 3=error $tran{subcode} = $resp[1]; # response internal subcode $tran{rcode} = $resp[2]; # reason code $tran{rtext} = $resp[3]; # reason text $tran{authcode} = $resp[4]; # six-digit approval code $tran{avscode} = $resp[5]; # address verification codes $tran{transid} = $resp[6]; # system transaction id $tran{md5hash} = $resp[37]; # md5 hash returned $tran{cvvresp} = $resp[38]; # cvv2 response code (M = match) return %tran; } ### ### UpdatePayment() will update an existing billto record with the results ### of a successful credit card transaction. ### sub UpdatePayment (\%\%) { my $tref = shift; # reference to trans hash my $bref = shift; # reference to billto hash my %trans = %$tref; # transaction hash my %billto = %$bref; # billto hash my %db1 = (); # database handle my $rows = 0; # row counter # update the appropriate fields $billto{cctransid} = $trans{transid}; $billto{approval} = $trans{authcode}; $billto{paid} = $billto{due}; $billto{due} = "0.00"; # make sure we are connected to the database unless (ConnectDatabase()) { return 0; } # obtain the connection info %db1 = GetDatabaseInfo(); # preformat the data for entry into the database foreach (keys %billto) { unless (defined $billto{$_}) { $billto{$_} = ""; } $billto{$_} = $db1{dbh}->quote($billto{$_}); } # execute the update (orderid, sessid, and memid are not affected) $rows = $db1{dbh}->do (qq{ UPDATE billto SET cctransid = $billto{cctransid}, approval = $billto{approval}, paid = $billto{paid}, due = $billto{due}, updatedate = NOW() WHERE sessid = $billto{sessid} }); # check for error if ($DBI::err) { ErrorMsg ("error: unable to perform update: $DBI::err : $DBI::errstr"); return 0; } return 1; } ### ### CreateOrder() will convert the shopping cart into a formal order in the ### system and remove the shopping cart records. ### sub CreateOrder (\%\@) { my $oref = shift; # reference to orders hash my $rref = shift; # reference to cart record array my %orders = %$oref; # orders record hash my @recs = @$rref; # cart record array my %cart = (); # cart record hash my %orderd = (); # orderd record hash my %db1 = (); # database connection hash my $rows = 0; # row update count # process each record in the shopping cart foreach $i (@recs) { # convert the record into a hash %cart = DecodeCartRecord ($i); # create the order detail hash $orderd{orderdid} = 0; $orderd{orderid} = $orders{orderid}; $orderd{sessid} = $cart{sessid}; # <- we don't need this anymore $orderd{itemid} = $cart{itemid}; $orderd{itemno} = $cart{itemno}; $orderd{title} = $cart{title}; $orderd{optname1} = $cart{optname1}; $orderd{optval1} = $cart{optval1}; $orderd{optname2} = $cart{optname2}; $orderd{optval2} = $cart{optval2}; $orderd{optname3} = $cart{optname3}; $orderd{optval3} = $cart{optval3}; $orderd{quantity} = $cart{quantity}; $orderd{price} = $cart{price}; $orderd{updatedate} = $cart{updatedate}; $orderd{createdate} = ""; # update the database AddOrderdRecord (%orderd); if (CheckError()) { ErrorMsg ("Can not add cart record to order detail table."); DisplayError(); ExitProgram(); } # kill the cart record DeleteCartRecord ($cart{cartid}); if (CheckError()) { ErrorMsg ("Can not remove old shopping cart record from cart table."); DisplayError(); ExitProgram(); } } # make sure we are connected to the database unless (ConnectDatabase()) { return 0; } # obtain the connection info %db1 = GetDatabaseInfo(); # update the orders record status $rows = $db1{dbh}->do (qq{ UPDATE orders SET status = "1" WHERE sessid = "$orders{sessid}" }); # check for error if ($DBI::err) { ErrorMsg ("error: unable to update order status: $DBI::err : $DBI::errstr"); return 0; } # update the session id for payment $rows = $db1{dbh}->do (qq{ UPDATE session SET attrib = "2" WHERE sessid = "$orders{sessid}" }); # check for error if ($DBI::err) { ErrorMsg ("error: unable to update session: $DBI::err : $DBI::errstr"); return 0; } return 1; } ### ### EmailInvoice() delivers a copy of the sales invoice to the customer and ### to the seller. ### sub EmailInvoice (\%\%\%) { my $tref = shift; # reference to trans hash my $bref = shift; # reference to billto hash my $sref = shift; # reference to shipto hash my %tran = %$tref; # transaction hash my %billto = %$bref; # billto hash my %shipto = %$sref; # shipto hash my $date = ""; # todays date and time my $name = ""; # concatenated name my $itot = 0; # item total my $cnt = 0; # item counter my $i = ""; # loop iteration variable # create the customer name $name = $shipto{fname}; $name .= " $shipto{mname}" if ($shipto{mname}); $name .= " $shipto{lname}" if ($shipto{lname}); # get the time $date = localtime; # open sendmail open (EMI,"|$sendmail -f${smfrom} ${admin} $shipto{email1} $shipto{email2}") || return 0; # print the sales invoice print EMI < To: $name <$shipto{email1}> Subject: $storename Invoice SALES INVOICE For Order Information: $storename 1-800-582-8573 (toll free) PO Box 123 orders\@opencart.org Anytown, MD 12345 $date Order ID: $orders{orderid} Tran Code: $tran{transid} Auth Code: $tran{authcode} SOLD TO: $billto{name} EOF print EMI "$billto{company}\n" if ($billto{company}); print EMI "$billto{address1}\n" if ($billto{address1}); print EMI "$billto{address2}\n" if ($billto{address2}); print EMI <

Invoice

 
Sales Invoice
 
Your order has been accepted. Please print this page to keep a copy of this transaction for your records. In addition, a copy of this invoice has been sent to you via email.  

 
Order ID: $orders{orderid}
Tran Code: $tran{transid}
Auth Code: $tran{authcode}
 
$storename
PO Box 123
Anytown, MD 12345
 
$date
 
SOLD TO:
$billto{name}
EOF print " $billto{company}
\n" if ($billto{company}); print " $billto{address1}
\n" if ($billto{address1}); print " $billto{address2}
\n" if ($billto{address2}); print <  
EOF print " $shipto{email1}
\n" if ($shipto{email1}); print " $shipto{email2}
\n" if ($shipto{email2}); print <
SHIP TO:
$shipto{fname} $shipto{mname} $shipto{lname}
EOF print " $shipto{company}
\n" if ($shipto{company}); print " $shipto{address1}
\n" if ($shipto{address1}); print " $shipto{address2}
\n" if ($shipto{address2}); print <  
EOF print " Phone Day: $shipto{phoneday}
\n" if ($shipto{phoneday}); print " Phone Eve: $shipto{phoneeve}
\n" if ($shipto{phoneeve}); print <
EOF # if we have shipping notes, print them here if ($shipto{shipnotes}) { print < Shipping Notes: $shipto{shipnotes}
 

EOF } # display column headings print < Qty Item No Item Description Each   Total  
EOF # process each item in the shopping cart foreach $i (@recs) { # decode the record into a hash %cart = DecodeCartRecord ($i); # compute totals $itot = ($cart{price} * $cart{quantity}); $itot = sprintf ("%4.2f",$itot); # output the row print < $cart{quantity} $cart{itemno} $cart{title} $cart{price}   $itot   EOF } # foreach # show totals print <
EOF # now print the total lines print <     Cart Subtotal  $orders{itemtot}       Sales Tax  $orders{tax}       Shipping  $orders{shipcost}  
    Order Total  $orders{total}  
 
By placing your order, you agree that:
  1. You are at least eighteen (18) years or age or older.

  2. You have the authorization to charge the credit card you have specified for a total amount of \$$orders{total}.

  3. You have read and agree with the terms and conditions described in our ordering policies, our shipping information, and the FAQ section of our web site.

Thank you for your order!  

 
EOF OutputPageBottom(); return 1; } ### ### DisplayPaymentDeclined() displays a payment declined page to the customer. ### The customer is given the alternative to enter another credit card or ### cancel the order. ### sub DisplayPaymentDeclined (\%) { my $tref = shift; # reference to trans hash my %tran = %$tref; # transaction hash my $msg = ""; # error message type # set the message if ($tran{status} == 1) { $msg = "OK"; } elsif ($tran{status} == 2) { $msg = "DECLINED"; } else { $msg = "ERROR"; } # output the top part of the page SetPageSize (560,560); OutputPageTop("Order Declined"); # put a heading on the page print <
 
Credit Card Declined
 
Your credit card was declined for the reason noted below. If you have another credit card, you can click the Continue button to enter another card for your purchase. To cancel the order, click the Cancel button.
 
Problem: $msg
Reason: $tran{rtext}
 
   
 
EOF OutputPageBottom(); return 1; } ### ### GetOrdersRecord() retrieves the order record from the database. It ### returns a hash of the fields returned, or undef if an error occurs. ### Error messages are returned through the application error table. This ### function requires the session id as a parameter. ### sub GetOrdersRecord ($) { my $sessid = shift; # session id of shopping cart my %out = (); # result output hash my %db1 = (); # database connection hash my $ref = ""; # hash reference # make sure we are connected to the database unless (ConnectDatabase()) { return %out; } # obtain the connection info %db1 = GetDatabaseInfo(); # prepare the select statement $db1{sth} = $db1{dbh}->prepare (qq{ SELECT * FROM orders WHERE sessid = "$sessid" }); # check for error if ($DBI::err) { ErrorMsg ("Unable to prepare select: $DBI::err : $DBI::errstr"); return %out; } # execute the statement $db1{sth}->execute (); # check for error if ($DBI::err) { ErrorMsg ("Unable to execute select: $DBI::err : $DBI::errstr"); return %out; } # retrieve a pointer to the record hash $ref = $db1{sth}->fetchrow_hashref; # check for record not found if (!defined $ref) { return %out; } %out = %{$ref}; $db1{sth}->finish (); return %out; } ### ### GetShiptoRecord() retrieves the shipto record from the database. It ### returns a hash of the fields returned, or undef if an error occurs. ### Error messages are returned through the application error table. This ### function requires the session id as a parameter. ### ### Data from this record is used to populate new billing records. ### sub GetShiptoRecord ($) { my $sessid = shift; # session id of shopping cart my %out = (); # result output hash my %db1 = (); # database connection hash my $ref = ""; # hash reference # make sure we are connected to the database unless (ConnectDatabase()) { return %out; } # obtain the connection info %db1 = GetDatabaseInfo(); # prepare the select statement $db1{sth} = $db1{dbh}->prepare (qq{ SELECT * FROM shipto WHERE sessid = "$sessid" }); # check for error if ($DBI::err) { ErrorMsg ("Unable to prepare select: $DBI::err : $DBI::errstr"); return %out; } # execute the statement $db1{sth}->execute (); # check for error if ($DBI::err) { ErrorMsg ("Unable to execute select: $DBI::err : $DBI::errstr"); return %out; } # retrieve a pointer to the record hash $ref = $db1{sth}->fetchrow_hashref; # check for record not found if (!defined $ref) { return %out; } %out = %{$ref}; $db1{sth}->finish (); return %out; } ### ### GetBilltoRecord() retrieves the billto record from the database. It ### returns a hash of the fields returned, or undef if an error occurs. ### Error messages are returned through the application error table. This ### function requires the session id as a parameter. ### sub GetBilltoRecord ($) { my $sessid = shift; # session id of shopping cart my %out = (); # result output hash my %db1 = (); # database connection hash my $ref = ""; # hash reference # make sure we are connected to the database unless (ConnectDatabase()) { return %out; } # obtain the connection info %db1 = GetDatabaseInfo(); # prepare the select statement $db1{sth} = $db1{dbh}->prepare (qq{ SELECT * FROM billto WHERE sessid = "$sessid" }); # check for error if ($DBI::err) { ErrorMsg ("Unable to prepare select: $DBI::err : $DBI::errstr"); return %out; } # execute the statement $db1{sth}->execute (); # check for error if ($DBI::err) { ErrorMsg ("Unable to execute select: $DBI::err : $DBI::errstr"); return %out; } # retrieve a pointer to the record hash $ref = $db1{sth}->fetchrow_hashref; # check for record not found if (!defined $ref) { return %out; } %out = %{$ref}; $db1{sth}->finish (); return %out; } ### ### ### AddOrderdRecord() will add a new record to the database. If an error is ### detected during the request, it is returned in the error table. This ### routine returns the 1 if successful, or 0 if an error occurred. ### sub AddOrderdRecord (\%) { my ($arg1) = @_; # tables passed by reference my %fields = %{$arg1}; # field name and value hash my %db1 = (); # database connection hash my $rows = 0; # row counter # make sure we are connected to the database unless (ConnectDatabase()) { return 0; } # obtain the connection info %db1 = GetDatabaseInfo(); # preformat the data for entry into the database foreach (keys %fields) { unless (defined $fields{$_}) { $fields{$_} = ""; } $fields{$_} = $db1{dbh}->quote($fields{$_}); } # execute the insert $rows = $db1{dbh}->do (qq{ INSERT INTO orderd SET orderdid = NULL, orderid = $fields{orderid}, sessid = $fields{sessid}, itemid = $fields{itemid}, itemno = $fields{itemno}, title = $fields{title}, optname1 = $fields{optname1}, optval1 = $fields{optval1}, optname2 = $fields{optname2}, optval2 = $fields{optval2}, optname3 = $fields{optname3}, optval3 = $fields{optval3}, quantity = $fields{quantity}, price = $fields{price}, updatedate = NOW(), createdate = NOW() }); # check for error if ($DBI::err) { ErrorMsg ("error: unable to perform insert: $DBI::err : $DBI::errstr"); return 0; } return 1; }