/
SagePay.pm
1249 lines (1003 loc) · 51.8 KB
/
SagePay.pm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Vend::Payment::SagePay - Interchange Sagepay support
#
# SagePay.pm, v 0.8.7, May 2009
#
# Copyright (C) 2009 Zolotek Resources Ltd. All rights reserved.
#
# Author: Lyn St George <info@zolotek.net, http://www.zolotek.net>
# Based on original code by Mike Heins <mheins@perusion.com> and others.
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public Licence as published by
# the Free Software Foundation; either version 2 of the Licence, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public Licence for more details.
#
# You should have received a copy of the GNU General Public
# Licence along with this program; if not, write to the Free
# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
# MA 02111-1307 USA.
package Vend::Payment::SagePay;
=head1 Interchange Sagepay Support
Vend::Payment::SagePay
http://kiwi.zolotek.net is the home page with the latest version.
=head1 This package is for the 'SagePay Direct' payment system.
Note that their 'Direct' system is the only one which leaves the customer on
your own site and takes payment in real time. Their other systems, eg Terminal
or Server, do not require this module.
=head1 Quick Start Summary
1 Place this module in <IC_root>/lib/Vend/Payment/SagePay.pm
2 Call it in interchange.cfg with:
Require module Vend::Payment::SagePay
3 Add into variable.txt (tab separated):
MV_PAYMENT_MODE sagepay
Add a new route into catalog.cfg (options for the last entry in parentheses):
Route sagepay id YourSagePayID
Route sagepay host live.sagepay.com (test.sagepay.com)
Route sagepay currency GBP (USD, EUR, others, defaults to GBP)
Route sagepay txtype PAYMENT (AUTHENTICATE, DEFERRED)
Route sagepay available yes (no, empty)
Route sagepay logzero yes (no, empty)
Route sagepay logorder yes (no, empty)
Route sagepay logsagepay yes (no, empty)
Route sagepay applyavscv2 '0': if enabled then check, and if rules apply use.
'1': force checks even if not enabled; if rules apply use.
'2': force NO checks even if enabled on account.
'3': force checks even if not enabled; do NOT apply rules.
Route sagepay giftaidpayment 0 (1 to donate tax to Gift Aid)
4 Create a new locale setting for en_GB as noted in "item currency" below, and copy the
public space interchange/en_US/ directory to a new interchange/en_GB/ one. Ensure that any
other locales you might use have a correctly named directory as well. Ensure that this locale
is found in your version of locale.txt (and set up GB as opposed to US language strings to taste).
5 Create entry boxes on your checkout page for: 'mv_credit_card_issue_number', 'mv_credit_card_start_month',
'mv_credit_card_start_year', 'mv_credit_card_type' and 'mv_credit_card_cvv2'.
6 The new fields in API 2.23 are: BillingAddress, BillingPostCode, DeliveryAddress, DeliveryPostCode,
BillingFirstnames, BillingSurname, DeliveryFirstnames, DeliverySurname, ContactNumber,ContactFax,CustomerEmail.
CustomerName has been removed. Billing and Delivery State must be sent if the destination country is the US, otherwise
they are not required. State must be only 2 letters if sent. Other fields may default to a space if there
is no proper value to send, though this may conflict with your AVS checking rules. SagePay currently
accept a space as of time of writing - if they change this without changing the API version then send
either a series of '0' or '-' characters to stop their error messages.
7. Add a page in pages/ord/, tdsfinal.html, being a minimal page with only the header and side bar,
and in the middle of the page put:
[if scratch acsurl]
<tr>
<td align=center height=600 valign=middle colspan=2>
<iframe src="__CGI_URL__/ord/tdsauth.html" frameborder=0 width=600 height=600></iframe>
</td>
</tr>
[/if]
Add a page in pages/ord/, tdsauth.html, consisting of this:
<body onload="document.form.submit();">
<FORM name="form" action="[scratchd acsurl]" method="POST" />
<input type="hidden" name="PaReq" value="[scratch pareq]" />
<input type="hidden" name="TermUrl" value="[scratch termurl]" />
<input type="hidden" name="MD" value="[scratch md]" />
</form>
</body>
along with whatever <noscript> equivalent you want. This will retrieve the bank's page within the iframe.
Add a page in pages/ord/, tdsreturn.html, consisting of this:
[charge route="sagepay" sagepayrequest="3dsreturn"]
<p>
<blockquote>
<font color="__CONTRAST__">
[error all=1 keep=1 show_error=1 show_label=1 joiner="<br>"]
</font>
</blockquote>
The iframe in 'tdsfinal' will be populated with the contents of 'tdsauth', and the javascript will
automatically display the bank's authentication page. When the customer clicks 'Submit' at the bank's
page, the iframe contents will be replaced with the 'tdsreturn' page, which will complete the route
and display the receipt inside the iframe. If the customer clicks 'cancel' at the bank, then this
'tdsreturn' page will stay put and display whatever message you have put there along with the error message.
The value of [scratch tds] is set true for a 3DSecure transaction only, so can be used for messages
etc on the receipt page.
8. When running a card through 3DSecure, the route is run twice: firstly to Sagepay who check whether or
not the card is part of 3DSecure - if it is they send the customer to the bank's authentication page
and upon returning from that the route must be run a second time to send the authentication results to
Sagepay. The second run is initiated from the 'ord/tdsreturn' page, not from etc/log_transaction as it normally
would be. To handle this change to the normal system flow you need to alter log_transaction to make the
call to the payment module conditional,ie, wrap the following code around the "[charge route...]" call
found in ln 172 (or nearby):
[if scratchd mstatus eq success]
[tmp name="charge_succeed"][scratch order_id][/tmp]
[else]
[tmp name="charge_succeed"][charge route="[var MV_PAYMENT_MODE]" amount="[scratch tmp_remaining]" order_id="[value mv_transaction_id]"][/tmp]
[/else]
[/if]
If the first call to Sagepay returns a request to send the customer to the 3DSecure server, then IC will
write a payment route error to the error log prior to sending the customer there. This error stops the
route completing and lets the 3DSecure process proceed as it should. This error is not raised if the card
is not part of 3DSecure, and instead the route completes as it normally would.
Also add this line just after '&final = yes' near the end of the credit_card section of etc/profiles.order:
&set=mv_payment_route sagepay
9. Add these new fields into log_transaction, to record the values returned from Sagepay (these will be
key in identifying transactions and problems in any dispute with them):
mv_credit_card_type: [calc]$Session->{payment_result}{CardType}[/calc]
mv_credit_card_issue_number: [value mv_credit_card_issue_number]
txtype: [calc]$Session->{payment_result}{TxType};[/calc]
vpstxid: [calc]$Session->{payment_result}{VPSTxID};[/calc]
txauthno: [calc]$Session->{payment_result}{TxAuthNo};[/calc]
securitykey: [calc]$Session->{payment_result}{SecurityKey};[/calc]
vendortxcode: [calc]$Session->{payment_result}{VendorTxCode};[/calc]
avscv2: [calc]$Session->{payment_result}{AVSCV2};[/calc]
addressresult:[calc]$Session->{payment_result}{AddressResult};[/calc]
postcoderesult: [calc]$Session->{payment_result}{PostCodeResult};[/calc]
cv2result: [calc]$Session->{payment_result}{CV2Result};[/calc]
securestatus:[calc]$Session->{payment_result}{SecureStatus};[/calc]
pares: [calc]$Session->{payment_result}{PaRes};[/calc]
md: [calc]$Session->{payment_result}{MD};[/calc]
cavv: [calc]$Session->{payment_result}{CAVV};[/calc]
and add these into your MySQL or Postgres transactions table, as type varchar(128) except for 'pares'
which should be type 'text'.
Note that there is no 'TxAuthNo' returned for a successful AUTHENTICATE.
=head1 PREREQUISITES
Net::SSLeay
or
LWP::UserAgent and Crypt::SSLeay
wget - a recent version built with SSL and supporting the 'connect' timeout function.
=head1 DESCRIPTION
The Vend::Payment::SagePay module implements the SagePay() routine for use with
Interchange. It is _not_ compatible on a call level with the other Interchange
payment modules - SagePay does things rather differently.
To enable this module, place this directive in C<interchange.cfg>:
Require module Vend::Payment::SagePay
This I<must> be in interchange.cfg or a file included from it.
Make sure CreditCardAuto is off (default in Interchange demos).
=head1 The active settings.
The module uses several of the standard settings from the Interchange payment routes.
Any such setting, as a general rule, is obtained first from the tag/call options on
a page, then from an Interchange order Route named for the mode in catalog.cfg,
then a default global payment variable in products/variable.txt, and finally in
some cases a default will be hard-coded into the module.
=over
=item Mode
The mode can be named anything, but the C<gateway> parameter must be set
to C<sagepay>. To make it the default payment gateway for all credit card
transactions in a specific catalog, you can set in C<catalog.cfg>:
Variable MV_PAYMENT_MODE sagepay
or in variable.txt:
MV_PAYMENT_MODE sagepay (tab separated)
if you want this to cooperate with other payment systems, eg PaypalExpress, then see the documentation
that comes with that system - it should be fully explained there.
=item id
Your SagePay vendor ID, supplied by SagePay when you sign up. Various ways to state this:
in variable.txt:
MV_PAYMENT_ID YourSagePayID Payment
or in catalog.cfg either of:
Route sagepay id YourSagePayID
Variable MV_PAYMENT_ID YourSagePayID
or on the page
[charge route=sagepay id=YourSagePayID]
=item txtype
The transaction type is one of: PAYMENT, AUTHENTICATE, DEFERRED for an initial purchase
through the catalogue, and then can be one of: AUTHORISE, REFUND, RELEASE, VOID, ABORT for payment
operations through the virtual terminal.
The transaction type is taken firstly from a dynamic variable in the page, meant
primarily for use with the 'virtual payment terminal', viz: 'transtype' in a select box
though this could usefully be taken from a possible entry in the products database
if you have different products to be sold on different terms; then falling back to
a 'Route txtype PAYMENT' entry in catalog.cfg; then falling back to a global
variable in variable.txt, eg 'MV_PAYMENT_TXTYPE PAYMENT Payment'; and finally
defaulting to 'PAYMENT' hard-coded into the module. This variable is returned to
the module and logged using the value returned from SagePay, rather than a value from
the page which possibly may not exist.
=item available
If 'yes', then the module will check that the gateway is responding before sending the transaction.
If it fails to respond within 9 seconds, then the module will go 'off line' and log the transaction
as though this module had not been called. It will also log the txtype as 'OFFLINE' so that you
know you have to put the transaction through manually later (you will need to capture the card
number to do this). The point of this is that your customer has the transaction done and dusted,
rather than being told to 'try again later' and leaving for ever. If not explicitly 'yes',
defaults to 'no'. NB: if you set this to 'yes', then add into the etc/report that is sent to you:
Txtype = [calc]$Session->{payment_result}{TxType};[/calc]. Note that you need to have
a recent version of wget which supports '--connect-timeout' to run this check. Note also that,
as this transaction has not been logged anywhere on the SagePay server, you cannot use their
terminal to process it. You must use a virtual terminal which includes a function for this purpose,
and updates the existing order number with the new payment information returned from SagePay. Note
further that if you have SagePay set up to require the CV2 value, then virtual terminal should disable
CV2 checking at run-time by default for such a transaction (logging the CV2 value breaks Visa/MC
rules and so it can't be legally available for this process).
=item logzero
If 'yes', then the module will log a transaction even if the amount sent is zero (which the
gateway would normally reject). The point of this is to allow a zero amount in the middle of a
subscription billing series for audit purposes. If not explicitly 'yes', defaults to 'no'.
Note: this is only useful if you are using an invoicing system or the Payment Terminal, both of which
by-pass the normal IC processes. IC will allow an item to be processed at zero total price but simply
bypasses the gateway when doing so.
=item logempty
If 'yes, then if the response from SagePay is read as empty (ie, zero bytes) then the module will use the
VendorTxID to check on the Sagepay txstatus page to see if that transaction has been logged. If it has then
the result found on that page will be used to push the result to either success or failure and log accordingly.
There are two markers set to warn of this:
$Session->{payment_result}{TxType} will be NULL,
$Session->{payment_result}{StatusDetail} will be: 'UNKNOWN status - check with SagePay before dispatching goods'
and you should include these into the report emailed to you. It will also call a logorder Usertag to log
a backup of the order: if you don't already have this then get it from ftp.zolotek.net/mv/logorder.tag
If the result is not found on that txstatus page then the result is forced to 'failure' and the transaction
shown as failed to the customer.
=item card_type
SagePay requires that the card type be sent. Valid types are: VISA, MC, AMEX, DELTA, SOLO, MAESTRO, UKE,
JCB, DINERS (UKE is Visa Electron issued in the UK).
You may display a select box on the checkout page like so:
<select name=mv_credit_card_type>
[loop
option=mv_credit_card_type
acclist=1
list=|
VISA=Visa,
MC=MasterCard,
SOLO=Solo,
DELTA=Delta,
MAESTRO=Maestro,
AMEX=Amex,
UKE=Electron,
JCB=JCB,
DINERS=Diners|]
<option value="[loop-code]"> [loop-param label]
[/loop]
</select>
=item currency
SagePay requires that a currency code be sent, using the 3 letter ISO currency code standard,
eg, GBP, EUR, USD. The value is taken firstly from either a page setting or a
possible value in the products database, viz 'iso_currency_code'; then falling back
to the locale setting - for this you need to add to locale.txt:
code en_GB en_EUR en_US
iso_currency_code GBP EUR USD
It then falls back to a 'Route sagepay currency EUR' type entry in catalog.cfg;
then falls back to a global variable (eg MV_PAYMENT_CURRENCY EUR Payment); and
finally defaults to GBP hard-coded into the module. This variable is returned to
the module and logged using the value returned from SagePay, rather than a value from
the page which possibly may not exist.
=item cvv2
This is sent to SagePay as mv_credit_card_cvv2. Put this on the checkout page:
<b>CVV2: <input type=text name=mv_credit_card_cvv2 size=6></b>
but note that under Card rules you must not log this value anywhere.
=item issue_number
This is used for some debit cards, and taken from an input box on the checkout page:
Card issue number: <input type=text name=mv_credit_card_issue_number value='' size=6>
=item mvccStartDate
This is used for some debit cards, and is taken from select boxes on the
checkout page in a similar style to those for the card expiry date. The labels to be
used are: 'mv_credit_card_start_month', 'mv_credit_card_start_year'. Eg:
<select name=mv_credit_card_start_year>
[loop option=start_date_year lr=1 list=`
my $year = $Tag->time( '', { format => '%Y' }, '%Y' );
my $out = '';
for ($year - 7 .. $year) {
/\d\d(\d\d)/;
$last_two = $1;
$out .= "$last_two\t$_\n";
}
return $out;
`]
<option value="[loop-code]"> [loop-pos 1]
[/loop]
</select>
Make the select box for the start month a copy of the existing one for the expiry month, but with
the label changed and the addition of
= --select --,
as the first entry. This intentionally returns nothing for that selection and prevents the StartDate being sent.
=item SagePay API v2.23 extra functions
ApplyAVSCV2 set to:
0 = If AVS/CV2 enabled then check them. If rules apply, use rules. (default)
1 = Force AVS/CV2 checks even if not enabled for the account. If rules apply, use rules.
2 = Force NO AVS/CV2 checks even if enabled on account.
3 = Force AVS/CV2 checks even if not enabled for the account but DON'T apply any rules.
You may pass this value from the page as 'applyavscv2' to override the payment route setting.
They also have Paypal integrated into this version, but I have no interest in implementing Paypal
through Sagepay. There is a separate PaypalExpress module for that.
ContactFax: optional
GiftAidPayment: set to -
0 = This transaction is not a Gift Aid charitable donation(default)
1 = This payment is a Gift Aid charitable donation and the customer has AGREED to donate the tax.
You may pass this value from the page as 'giftaidpayment'
ClientIPAddress: will show in SagePay reports, and they will attempt to Geo-locate the IP.
=item AVSCV2
SagePay do not use your rulebase or return any checks for these when using 3ds and AUTHORISE. As this data
is essential for many business models you should use DEFERRED instead. While thought was given to
running a PAYMENT and VOID for £1 first, simply to get the AVSCV2 results, this cannot be done
with Maestro cards which legally must go through 3ds and so I have abandoned the idea.
=item Encrypted email with card info
If you want to add the extra fields (issue no, start date) to the PGP message
emailed back to you, then set the following in catalog.cfg:
Variable<tab>MV_CREDIT_CARD_INFO_TEMPLATE Card type: {MV_CREDIT_CARD_TYPE}; Card no: {MV_CREDIT_CARD_NUMBER}; Expiry: {MV_CREDIT_CARD_EXP_MONTH}/{MV_CREDIT_CARD_EXP_YEAR}; Issue no: {MV_CREDIT_CARD_ISSUE_NUMBER}; StartDate: {MV_CREDIT_CARD_START_MONTH}/{MV_CREDIT_CARD_START_YEAR}
=item testing
The SagePay test site is test.sagepay.com, and their live site is
live.sagepay.com. Enable one of these in MV_PAYMENT_HOST in variable.txt
(*without* any leading https://) or as 'Route sagepay host test.sagepay.com' in
catalog.cfg. Be aware that the test site is not an exact replica of the live site, and errors there
can be misleading. In particular the "SecureStatus" returned from the test site is
liable to be 'NOTAUTHED' when the live site will return 'OK'.
=item methods
An AUTHENTICATE will check that the card is not stolen and contains sufficient funds.
SagePay will keep the details, so that you may settle against this a month or more
later. Against an AUTHENTICATE you may do an AUTHORISE (which settles the transaction).
A DEFERRED will place a shadow ('block') on the funds for seven days (or so, depending
on the acquiring bank). Against a DEFERRED you may do a RELEASE to settle the transaction.
A PAYMENT will take the funds immediately. Against a PAYMENT, you may do a
REFUND or REPEAT.
A REPEAT may be performed against an AUTHORISE or a PAYMENT. This will re-check and
take the funds in real time. You may then REPEAT a REPEAT, eg for regular
subscriptions. As you need to send the amount and currency with each REPEAT, you
may vary the amount of the REPEAT to suit a variation in subscription fees.
A RELEASE is performed to settle a DEFERRED. Payment of the originally specified
amount is guaranteed if the RELEASE is performed within the seven days for which
the card-holder's funds are 'blocked'.
A REFUND may be performed against a PAYMENT, RELEASE, or REPEAT. It may be for a
partial amount or the entire amount, and may be repeated with several partial
REFUNDs so long as the total does not exceed the original amount.
A DIRECTREFUND sends funds from your registered bank account to the nominated credit card.
This does not need to refer to any previous transaction codes, and is useful if you need to
make a refund but the customer's card has changed or the original purchase was not made by card.
=back
=head2 Troubleshooting
Try a sale with any other test number given by SagePay, eg:
Visa VISA 4929 0000 0000 6
Mastercard MC 5404 0000 0000 0001
Delta DELTA 4462 0000 0000 0000 0003
Visa Electron UK Debit UKE 4917300000000008
Solo SOLO 6334 9000 0000 0000 0005 issue no 1
Switch (UK Maestro) MAESTRO 5641 8200 0000 0005 issue no 01.
Maestro MAESTRO 300000000000000004
AmericanExpress AMEX 3742 0000 0000 004
You need these following values to ensure a positive response:
CV2: 123
Billing Address: 88
Billing PostCode: 412
and the password at their test server is 'password'.
If nothing works:
=over 4
=item *
Make sure you "Require"d the module in interchange.cfg:
Require module Vend::Payment::SagePay
=item *
Make sure either Net::SSLeay or Crypt::SSLeay and LWP::UserAgent are installed
and working. You can test to see whether your Perl thinks they are:
perl -MNet::SSLeay -e 'print "It works\n"'
or
perl -MLWP::UserAgent -MCrypt::SSLeay -e 'print "It works\n"'
If either one prints "It works." and returns to the prompt you should be OK
(presuming they are in working order otherwise).
=item *
Check the error logs, both catalogue and global. Make sure you set your payment
parameters properly. Try an order, then put this code in a page:
<XMP>
[calc]
my $string = $Tag->uneval( { ref => $Session->{payment_result} });
$string =~ s/{/{\n/;
$string =~ s/,/,\n/g;
return $string;
[/calc]
</XMP>
That should show what happened.
=item *
If you have a PGP/GPG failure when placing an order through your catalogue
then this may cause the module to be immediately re-run. As the first run would
have been successful, meaning that both the basket and the credit card information
would have been emptied, the second run will fail. The likely error message within
the catalogue will be:
"Can't figure out credit card expiration". Fixing PGP/GPG will fix this error.
If you get the same error message within the Virtual Terminal, then you haven't
set the order route as noted above.
=item *
If all else fails, Zolotek and other consultants are available to help
with integration for a fee.
=back
=head1 AUTHORS
Lyn St George <info@zolotek.net>, based on original code by Mike Heins
<mike@perusion.com> and others.
=head2 CREDITS
Hillary Corney (designersilversmiths.co.uk), Jamie Neil (versado.net),
Andy Mayer (andymayer.net) for testing and suggestions.
=cut
BEGIN {
my $selected;
eval {
package Vend::Payment;
require Net::SSLeay;
import Net::SSLeay qw(post_https make_form make_headers);
$selected = "Net::SSLeay";
};
$Vend::Payment::Have_Net_SSLeay = 1 unless $@;
unless ($Vend::Payment::Have_Net_SSLeay) {
eval {
package Vend::Payment;
require LWP::UserAgent;
require HTTP::Request::Common;
require Crypt::SSLeay;
require CGI;
require Encode;
import Encode qw(encode decode);
import HTTP::Request::Common qw(POST);
$selected = "LWP and Crypt::SSLeay";
};
$Vend::Payment::Have_LWP = 1 unless $@;
}
unless ($Vend::Payment::Have_Net_SSLeay or $Vend::Payment::Have_LWP) {
die __PACKAGE__ . " requires Net::SSLeay or Crypt::SSLeay";
}
::logGlobal("%s 0.8.8 payment module initialised, using %s", __PACKAGE__, $selected)
unless $Vend::Quiet;
}
package Vend::Payment;
use strict;
sub sagepay {
my $sagepaystart = time;
my $date = $Tag->time({ body => "%Y%m%d%H%M%S" });
my $sagepaydate = $Tag->time({ body => "%A %d %B %Y, %k:%M:%S, %Z" });
my ($vendor, $amount, $actual, $opt, $sagepayrequest, $page, $vendorTxCode, $pan, $cardType);
# Amount sent to SagePay, in 2 decimal places with cruft removed.
# Defaults to 'amount' from log_transaction or an invoicing system, falling back to IC input
$amount = $::Values->{amount} || charge_param('amount') || Vend::Interpolate::total_cost();
$amount =~ s/^\D*//g;
$amount =~ s/\,//g;
$amount = sprintf '%.2f', $amount;
# Transaction type sent to SagePay.
my $txtype = $::Values->{transtype} || charge_param('txtype') || $::Variable->{MV_PAYMENT_TRANSACTION} ||'PAYMENT';
my $vpsprotocol = '2.23';
my $accountType = $::Values->{account_type} || charge_param('account_type') || 'E';
my $payID = $::Values->{inv_no} || $::Session->{mv_transaction_id} || $::Session->{id}.$amount;
my $logorder = charge_param('logorder') || 'no'; # Set to 'yes' or '1' to log basket plus data useful when arguing with SagePay over empty responses
my $logsagepay = charge_param('logsagepay') || 'no'; # Set to yes or 1 to log sagepay activity for debugging
my $logzero = charge_param('logzero') || 'no';
my $available = $::Values->{available} || charge_param('available') || 'no';
my $description = "$::Values->{company} $::Values->{fname} $::Values->{lname}";
$description = substr($description,0,99);
my $apply3ds = $::Values->{apply3ds} || charge_param('apply3ds') || '0'; # '2' will turn 3ds off, '0' is default live variant
my $applyAVSCV2 = $::Values->{applyavscv2} || charge_param('applyavscv2') || '0';
my $termurl = charge_param('returnurl') || "$::Variable->{SECURE_SERVER}$::Variable->{CGI_URL}/ord/tdsreturn";
my $tdscallback = charge_param('tdscallback') || '/gateway/service/direct3dcallback.vsp';
my $checkouturl = charge_param('checkouturl') || "$::Variable->{SECURE_SERVER}$::Variable->{CGI_URL}/ord/checkout";
my $checkstatus = charge_param('check_status') || '1';
my $checkstatusurl = charge_param('check_status_url') || '/TxStatus/TxStatus.asp';
my $allowmaestro = charge_param('allowmaestro'); # Allow Maestro card transactions to be logged offline - you will need a MOTO a/c to convert to online without 3DS.
#::logDebug("SP".__LINE__.": apply3ds=$apply3ds; avscv2=$applyAVSCV2; txtype=$txtype. $::Values->{transtype}, $tx");
$::Scratch->{tds} = '';
$::Scratch->{mstatus} = '';
$::Values->{amount} = '';
$::Values->{transtype} = '';
$::Values->{inv_no} = '';
$::Values->{apply3ds} = '';
$::Values->{applyavsvc2} = '';
$::Values->{account_type} = '';
my $billingState;
my $deliveryState;
my %result;
my %query;
my (%actual) = map_actual();
$actual = \%actual;
$opt = {};
$vendor = $opt->{id} || charge_param('id') || $::Variable->{MV_PAYMENT_ID};
$opt->{host} = charge_param('host') || $::Variable->{MV_PAYMENT_HOST} || 'live.sagepay.com';
$sagepayrequest = $opt->{sagepayrequest} = charge_param('sagepayrequest') || 'post';
$opt->{use_wget} = charge_param('use_wget') || '1';
$opt->{port} = '443';
#::logDebug("SP".__LINE__.": host=$opt->{host}; spreq=$sagepayrequest");
if ($txtype =~ /DEFERRED|PAYMENT|AUTHENTICATE|DEFAUTH/i) {
$opt->{script} = '/gateway/service/vspdirect-register.vsp';
}
elsif ($txtype =~ /RELEASE/i) {
$opt->{script} = '/gateway/service/release.vsp';
}
elsif ($txtype =~ /DIRECTREFUND/i) {
$opt->{script} = '/gateway/service/directrefund.vsp';
}
elsif ($txtype =~ /REFUND/i) {
$opt->{script} = '/gateway/service/refund.vsp';
}
elsif ($txtype =~ /VOID/i) {
$opt->{script} = '/gateway/service/void.vsp';
}
elsif ($txtype =~ /CANCEL/i) {
$opt->{script} = '/gateway/service/cancel.vsp';
}
elsif ($txtype =~ /ABORT/i) {
$opt->{script} = '/gateway/service/abort.vsp';
}
elsif ($txtype =~ /MANUAL/i) {
$opt->{script} = '/gateway/service/manualpayment.vsp';
}
elsif ($txtype =~ /REPEAT|REPEATDEFERRED/i) {
$opt->{script} = '/gateway/service/repeat.vsp';
}
elsif ($txtype =~ /AUTHORISE/i) {
$opt->{script} = '/gateway/service/authorise.vsp';
}
my @override = qw/
order_id
mv_credit_card_exp_month
mv_credit_card_exp_year
mv_credit_card_start_month
mv_credit_card_start_year
mv_credit_card_issue_number
mv_credit_card_number
/;
for(@override) {
next unless defined $opt->{$_};
$actual->{$_} = $opt->{$_};
}
#::logDebug("SP".__LINE__." actual map result: " . ::uneval($actual));
my $pan = $actual->{mv_credit_card_number} unless defined $pan;
$pan =~ s/\D//g;
$actual->{mv_credit_card_exp_month} =~ s/\D//g;
$actual->{mv_credit_card_exp_year} =~ s/\D//g;
$actual->{mv_credit_card_exp_year} =~ s/\d\d(\d\d)/$1/;
my $exp = sprintf '%02d%02d',
$actual->{mv_credit_card_exp_month}, $actual->{mv_credit_card_exp_year};
my $expshow = $exp;
$expshow =~ s/(\d\d)(\d\d)/$1\/$2/;
my $cardType = $actual->{mv_credit_card_type} || $CGI->{mv_credit_card_type} || $::Values->{mv_credit_card_type} unless defined $cardType;
$cardType = 'MC' if ($cardType =~ /mastercard/i);
my $mvccStartMonth = $actual->{mv_credit_card_start_month} || $::Values->{mv_credit_card_start_month} || $::Values->{start_date_month};
$mvccStartMonth =~ s/\D//g;
my $mvccStartYear = $actual->{mv_credit_card_start_year} || $::Values->{mv_credit_card_start_year} || $::Values->{start_date_year};
$mvccStartYear =~ s/\D//g;
$mvccStartYear =~ s/\d\d(\d\d)/$1/;
my $mvccStartDate;
if ($mvccStartMonth == '') { $mvccStartDate = '';
}
else { $mvccStartDate = sprintf '%02d%02d', $mvccStartMonth, $mvccStartYear;
}
my $issue = $actual->{mv_credit_card_issue_number} || $::Values->{mv_credit_card_issue_number} || $::Values->{card_issue_number};
$issue =~ s/\D//g;
my $cvv2 = $actual->{mv_credit_card_cvv2} || $::Values->{cvv2};
$cvv2 =~ s/\D//g;
# override the configured AVSCV2/3DS settings when using a terminal
$applyAVSCV2 = '2' unless ($txtype =~ /PAYMENT|DEFERRED|AUTHORISE/i);
$apply3ds = '2' unless ($txtype =~ /PAYMENT|DEFERRED|AUTHENTICATE/i);
# State must be only 2 letters, and is only required for the US. Other required fields default to a space to keep Sagepay happy.
# Filtering is now strict as Sagepay are making arbitrary changes to what they deem acceptable.
my $cardHolder = "$actual->{b_fname} $actual->{b_lname}" || "$actual->{fname} $actual->{lname}";
$cardHolder =~ s/[^a-zA-Z0-9,. ]//gi;
my $billingSurname = $actual->{b_lname} || $actual->{lname};
$billingSurname =~ s/[^a-zA-Z0-9,. ]//gi;
my $billingFirstnames = $actual->{b_fname} || $actual->{fname};
$billingFirstnames =~ s/[^a-zA-Z0-9,. ]//gi;
my $billingCountry = $actual->{b_country} || $actual->{country} || 'GB';
$billingCountry = 'GB' if ($billingCountry =~ /UK/);
my $billingAddress1 = $actual->{b_address} || $actual->{address} || ' ';
$billingAddress1 =~ s/\// /g;
$billingAddress1 =~ s/[^a-zA-Z0-9,. ]//gi;
my $billingAddress2 = $actual->{b_address2};
$billingAddress2 =~ s/[^a-zA-Z0-9,. ]//gi;
my $billingPostCode = $actual->{b_zip} || $actual->{zip} || ' ';
$billingPostCode =~ s/[^a-zA-Z0-9 ]//gi;
$billingState = $actual->{b_state} || $actual->{state} || '';
my $billingCity = $actual->{b_city} || $actual->{city} || ' ';
$billingCity .= ", $billingState" unless $billingCountry =~ /US/i;
$billingCity =~ s/[^a-zA-Z0-9,. ]//gi;
undef $billingState unless $billingCountry =~ /US/i;
my $billingPhone = $actual->{b_phone} || $actual->{phone_day} || $actual->{phone_night};
$billingPhone =~ s/[\(\)]/ /g;
$billingPhone =~ s/[^0-9-+ ]//g;
my $deliverySurname = $actual->{lname};
$deliverySurname =~ s/[^a-zA-Z0-9,. ]//gi;
my $deliveryFirstnames = $actual->{fname};
$deliveryFirstnames =~ s/[^a-zA-Z0-9,. ]//gi;
my $deliveryCountry = $actual->{country} || 'GB';
$deliveryCountry = 'GB' if ($deliveryCountry =~ /UK/);
my $deliveryPostCode = $actual->{zip} || ' ';
$deliveryPostCode =~ s/[^a-zA-Z0-9 ]//gi;
my $deliveryAddress1 = $actual->{address} || ' ';
$deliveryAddress1 =~ s/\// /gi;
$deliveryAddress1 =~ s/[^a-zA-Z0-9,. ]//gi;
my $deliveryAddress2 = $actual->{address2};
$deliveryAddress2 =~ s/[^a-zA-Z0-9,. ]//gi;
$deliveryState = $actual->{state} || '';
my $deliveryCity = $actual->{city} || ' ';
$deliveryCity .= ", $deliveryState" unless $deliveryCountry =~ /US/i;
$deliveryCity =~ s/[^a-zA-Z0-9,. ]//gi;
undef $deliveryState unless $deliveryCountry =~ /US/i;
my $deliveryPhone = $actual->{phone_day} || $actual->{phone_night};
$deliveryPhone =~ s/[\(\)]/ /g;
$deliveryPhone =~ s/[^0-9-+ ]//g;
my $customerEmail = $actual->{email};
$customerEmail =~ s/[^a-zA-Z0-9.\@\-_]//gi;
my $contactFax = $::Values->{fax} || '';
$contactFax =~ s/[\(\)]/ /g;
$contactFax =~ s/[^0-9-+ ]//gi;
my $giftAidPayment = $::Values->{giftaidpayment} || charge_param('giftaidpayment') || '0';
my $authCode = $::Values->{authcode} || '';
my $clientIPAddress = $CGI::remote_addr if $CGI::remote_addr;
$::Values->{authcode} = '';
#::logDebug("SP".__LINE__.": bCity=$billingCity; mvccType=$cardType; start=$mvccStartDate; issue=$issue;");
# ISO currency code sent to SagePay, from the page or fall back to config files.
my $currency = $::Values->{iso_currency_code} || $::Values->{currency_code} || $Vend::Cfg->{Locale}{iso_currency_code} ||
charge_param('currency') || $::Variable->{MV_PAYMENT_CURRENCY} || 'GBP';
my $psp_host = $opt->{host};
my $convertoffline = charge_param('convertoffline');
#--- make the initial request to SagePay and get back the values for the ACS ---------------------------
if ($sagepayrequest eq 'post') {
$::Session->{sagepay}{CardRef} = $pan;
$::Session->{sagepay}{CardRef} =~ s/^(\d\d).*(\d\d\d\d)$/$1****$2/;
# vendorTxCode generated here in 'post', and retrieved from session later
my $order_id = $Tag->time({ body => "%Y%m%d%H%M%S" });
$order_id .= $::Session->{id};
if ($txtype =~ /RELEASE|VOID|ABORT|CANCEL/i) {
$::Session->{sagepay}{vendorTxCode} = $::Values->{OrigVendorTxCode}
}
else {
$::Session->{sagepay}{vendorTxCode} = $order_id
}
%query = (
TxType => $txtype,
Vendor => $vendor,
AccountType => $accountType,
VPSProtocol => $vpsprotocol,
Apply3DSecure => $apply3ds
);
if ($txtype =~ /RELEASE/i) {
$query{ReleaseAmount} = $amount;
}
if ($txtype =~ /REFUND|REPEAT|AUTHORISE/) {
$query{RelatedVPSTxID} = $::Values->{RelatedVPSTxID};
$query{RelatedVendorTxCode} = $::Values->{RelatedVendorTxCode};
$query{RelatedSecurityKey} = $::Values->{RelatedSecurityKey};
$query{Description} = $description;
$query{Amount} = $amount;
}
if ($txtype =~ /REFUND|REPEAT/) {
$query{RelatedTxAuthNo} = $::Values->{RelatedTxAuthNo};
$query{Currency} = $currency;
}
if ($txtype =~ /VOID|ABORT|CANCEL|RELEASE/i) {
$query{VPSTxId} = $::Values->{OrigVPSTxID};
$query{SecurityKey} = $::Values->{OrigSecurityKey};
}
if ($txtype =~ /VOID|ABORT|RELEASE/i) {
$query{TxAuthNo} = $::Values->{OrigTxAuthNo};
}
if ($txtype =~ /DIRECTREFUND|DEFERRED|PAYMENT|AUTHENTICATE|MANUAL/i) {
$query{CardType} = $cardType;
$query{CardNumber} = $pan;
$query{CardHolder} = $cardHolder;
$query{Description} = $description;
$query{Amount} = $amount;
$query{Currency} = $currency;
$query{ExpiryDate} = $exp;
}
if ($txtype =~ /PAYMENT|DEFERRED|AUTHENTICATE|MANUAL/i) {
$query{BillingFirstNames} = $billingFirstnames;
$query{BillingSurname} = $billingSurname;
$query{BillingAddress1} = $billingAddress1;
$query{BillingAddress2} = $billingAddress2 if $billingAddress2;
$query{BillingCity} = $billingCity;
$query{BillingPostCode} = $billingPostCode;
$query{BillingState} = $billingState if $billingState;
$query{BillingCountry} = $billingCountry;
$query{BillingPhone} = $billingPhone;
$query{DeliveryFirstNames} = $deliveryFirstnames;
$query{DeliverySurname} = $deliverySurname;
$query{DeliveryAddress1} = $deliveryAddress1;
$query{DeliveryAddress2} = $deliveryAddress2 if $deliveryAddress2;
$query{DeliveryCity} = $deliveryCity;
$query{DeliveryPostCode} = $deliveryPostCode;
$query{DeliveryState} = $deliveryState if $deliveryState;
$query{DeliveryCountry} = $deliveryCountry;
$query{DeliveryPhone} = $deliveryPhone;
$query{ContactFax} = $contactFax;
$query{CustomerEmail} = $customerEmail;
$query{GiftAidPayment} = $giftAidPayment;
$query{ClientIPAddress} = $clientIPAddress;
$query{CV2} = $cvv2;
}
if ($txtype =~ /PAYMENT|DEFERRED|AUTHORISE/i) {
$query{ApplyAVSCV2} = $applyAVSCV2;
}
$query{AuthCode} = $authCode if $authCode;
$query{StartDate} = $mvccStartDate if $mvccStartDate;
$query{IssueNumber} = $issue if $issue;
}
#--- return from ACS ------------------------------------------------------------------------
elsif ($sagepayrequest eq '3dsreturn') {
$::Values->{sagepayrequest} = '';
$opt->{script} = $tdscallback;
$result{PaRes} = $CGI->{'PaRes'};
$result{MD} = $CGI->{'MD'};
#::logDebug("SP".__LINE__.": New PaRes=$result{PaRes}\nMD=$result{MD}");
%query = (
MD => $result{MD},
PaRes => $result{PaRes}
);
}
#--- query status from admin panel ----------------------------------------------------
elsif ($sagepayrequest eq 'querystatus') {
my %statusquery = (
Vendor => $vendor,
VendorTxCode => charge_param('vendortxcode')
);
$opt->{script} = $checkstatusurl;
my $post = post_data($opt, \%statusquery);
my $response = $post->{status_line};
my $page = $post->{result_page};
#::logDebug("SP".__LINE__.": query page = $page\n");
for my $line (split /\r\n/, $page) {
$result{Status} = $1 if ($line =~ /^Status=(.*)/i);
$result{StatusDetail} = $1 if ($line =~ /StatusDetail=(.*)/i);
$result{TxType} = $1 if ($line =~ /TransactionType=(.*)/i);
$result{Authorised} = $1 if ($line =~ /Authorised=(.*)/i);
$result{TxAuthCode} = $1 if ($line =~ /VPSAuthCode of (\d+)/i);
$result{VPSTxId} = $1 if ($line =~ /VPSTxId=(.*)/i);
$result{Amount} = $1 if ($line =~ /Amount=(.*)/i);
$result{Currency} = $1 if ($line =~ /Currency=(.*)/i);
$result{ReceivedDate} = $1 if ($line =~ /Received=(.*)/i);
$result{BatchID} = $1 if ($line =~ /BatchID=(.*)/i);
$result{Settled} = $1 if ($line =~ /Settled=(.*)/i);
}
}
#--- common stuff again --------------------------------------------------------------
my ($post, $response, @page);
# Test for gateway availability, and if not available optionally go off-line and complete
# transaction for manual processing later. Also go off-line if amount is zero, so as to log the
# transaction and email a receipt for audit purposes (useful mainly for subscription billing).
my ($request, $in);
#::logDebug("SP".__LINE__.": available=$available, amount=$amount, order_route=$::Values->{mv_order_route}, logzero=$logzero");
if (($available =~ /y|1/i) and ($amount > 0) and ($::Values->{mv_order_route} !~ /ptipm_route/i)) {
my $CMD = '/usr/bin/wget -nv --spider -T9 -t1';
open (IN, "$CMD https://$psp_host 2>&1 |") || die "Could not open pipe to wget: $!\n";
$in = <IN>;
chop($in);
close(IN);
$in = 'test' if ($::CGI->{offline} eq 'yes'); # testing only, will force offline mode
if ($in =~ /^200 OK$/) {
$request = 'psp';
}
else {
$request = 'offline';
}
if (($cardType =~ /MAESTRO|SWITCH/i) and ($allowmaestro != '1')) {
$request = 'psp'; # Maestro must go through 3D Secure unless merchant a/c type is set to MOTO in a terminal
}
}
elsif (($::Values->{mv_order_route} =~ /ptipm_route/) and ($amount > 0)) {
$request = 'psp';
}
elsif (($available !~ /y|1/i) and ($amount > 0)) {
$request = 'psp';
}
elsif (($amount == 0) and ($logzero =~ /y|1/i)) {
$request = 'log';
}
$result{VendorTxCode} = $::Session->{sagepay}{vendorTxCode};
#::logDebug("SP".__LINE__.": in=$in; request=$request; cardtype=$cardType; vendorTxCode=$result{VendorTxCode}");
if ($request eq 'psp') {
# Run normal routine to SagePay
$query{VendorTxCode} = $::Session->{sagepay}{vendorTxCode};
#::logDebug("SP".__LINE__.": now for keys in query");
my @query;
foreach my $key (sort keys(%query)) {
#::logDebug(qq{Query to SagePay: "$key=$query{$key}"}); # nicely readable version of the string sent (careful: this line will log sensitive info)
push @query, "$key=$query{$key}";
}
my $string = join '&', @query; # replicates the string as actually sent: useful to quote this for debugging
#::logDebug("SP".__LINE__.": string to SagePay: $string");
$post = post_data($opt, \%query);
$response = $post->{status_line};
$page = $post->{result_page};
#::logDebug("SP".__LINE__.": response page:\n-------------------------\n$page \n---------------------------\nend of SagePay results page\n\n");
$result{TxType} = $txtype;
$result{Currency} = $currency;
$result{CardRef} = $::Session->{sagepay}{CardRef};
$result{CardType} = $cardType;
$result{ExpAll} = $exp;
$result{amount} = $amount;
$result{request} = $request;
$result{IP} = $clientIPAddress;