1 package Business::OnlinePayment::vSecureProcessing;
9 use Business::OnlinePayment;
10 use Business::OnlinePayment::HTTPS;
11 #use Net::SSLeay qw(post_http post_https make_headers make_form);
12 use vars qw($VERSION $DEBUG @ISA $me);
14 @ISA = qw(Business::OnlinePayment::HTTPS);
17 $me = 'Business::OnlinePayment::vSecureProcessing';
20 # $server: http://dvrotsos2.kattare.com
22 # mapping out all possible endpoints
23 # but this version will only be building out "charge", "void", & "credit"
24 my %payment_actions = (
26 path => '/vsg2/processpayment',
29 path => '/vsg2/processvoid',
32 path => '/vsg2/processrefund',
35 path => '/vsg2/processauth',
37 'authorize_cancel' => {
38 path => '/vsg2/processauthcancel',
41 path => '/vsg2/processcaptureonly',
44 path => '/vsg2/createtoken',
47 path => '/vsg2/deletetoken',
50 path => '/vsg2/querytoken',
52 'update_exp_date' => {
53 path => '/vsg2/updateexpiration',
56 path => '/vsg2/updatetoken',
61 my %action_mapping = (
62 'normal authorization' => 'charge',
64 'authorization only' => 'authorize',
65 'post authorization' => 'capture',
66 'reverse authorization' => 'authorize_cancel'
74 # inistialize standard B::OP attributes
76 $self->$_( '' ) for qw/authorization
84 # B::OP creates the following accessors:
85 # server, port, path, test_transaction, transaction_type,
86 # server_response, is_success, authorization,
87 # result_code, error_message,
90 env platform userid gid tid appid action reference_number cvv_response
91 avs_response risk_score txn_amount txn_date response_code
94 $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
98 $self->server($options{'server'});
100 $self->gid($options{'gid'});
102 $self->tid($options{'tid'});
104 $self->platform($options{'platform'});
106 $self->appid($options{'appid'});
108 $self->env((defined($options{'env'})) ? $options{'env'} : 'live'); # 'live'/'test'
116 my ($self,$content) = @_;
117 my %content = $self->content();
120 no warnings 'uninitialized';
122 # strip non-digits from card number
123 my $card_number = '';
124 if ( $content{card_number} ) {
125 $content{card_number} =~ s/\D//g;
128 # separate month and year values for expiry_date
129 if ( $content{expiration} ) {
130 ($content{exp_month}, $content{exp_year}) = split /\//, $content{expiration};
131 $content{exp_month} = sprintf "%02d", $content{exp_month};
132 $content{exp_year} = substr($content{exp_year},0,2) if ($content{exp_year} > 99);
135 if (!$content{'first_name'} || !$content{'last_name'} && $content{'name'}) {
136 ($content{'first_name'}, $content{'last_name'}) = split(' ', $content{'name'}, 2);
139 if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
140 $content{'street_number'} = $1;
143 warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
144 $self->content(%content);
147 sub process_content {
149 $self->clean_content();
150 my %content = $self->content();
151 $self->action(($action_mapping{lc $content{'action'}}) ? $action_mapping{lc $content{'action'}} : lc $content{'action'});
152 $self->path($payment_actions{ $self->action }{path})
153 unless length($self->path);
154 $self->appid($content{appid}) if (!$self->appid && $content{appid});
160 # inistialize standard B::OP attributes
161 $self->is_success(0);
162 $self->$_( '' ) for qw/authorization
167 # clean and process the $self->content info
168 $self->process_content();
169 my %content = $self->content;
170 my $action = $self->action();
172 my @acceptable_actions = ('charge', 'refund', 'void');
174 unless ( grep { $action eq $_ } @acceptable_actions ) {
175 croak "'$action' is not supported at this time.";
178 # fill out the template vars
179 my $template_vars = {
182 platform => $self->platform,
183 userid => $self->userid,
189 amount => $content{'amount'},
190 track1 => ($content{'track1'}) ? $content{'track1'} : '',
191 track2 => ($content{'track2'}) ? $content{'track2'} : '',
192 type => ($content{'description'}) ? $content{'description'} : '',
193 cf1 => ($content{'UDField1'}) ? $content{'UDField1'} : '',
194 cf2 => ($content{'UDField2'}) ? $content{'UDField2'} : '',
196 account_number => ($content{'card_number'}) ? $content{'card_number'} : '',
197 exp_month => $content{'exp_month'},
198 exp_year => $content{'exp_year'},
199 cvv => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
200 first_name => ($content{'first_name'}) ? $content{'first_name'} : '',
201 last_name => ($content{'last_name'}) ? $content{'last_name'} : '',
202 postal_code => ($content{'zip'}) ? $content{'zip'} : '',
203 street_address => ($content{'street_number'}) ? $content{'street_number'} : '',
204 industry_type => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
205 invoice_num => ($content{'invoice_number'}) ? $content{'invoice_number'} : '',
206 appid => $self->appid(),
207 recurring => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
208 response_code => ($content{'response_code'}) ? $content{'response_code'} : '',
209 reference_number=> ($content{'ref_num'}) ? $content{'ref_num'} : '',
210 token => ($content{'token'}) ? $content{'token'} : '',
211 receipt => ($content{'receipt'}) ? $content{'receipt'} : '',
212 transaction_date=> ($content{'txn_date'}) ? $content{'txn_date'} : '',
213 merchant_data => ($content{'merchant_data'}) ? $content{'merchant_data'} : '',
216 # we won't be using level2 nor level3. So I'm leaving them blank for now.
221 ship_tp_postal_code => '',
222 ship_from_postal_code => '',
224 product_description1 => '',
225 product_description2 => '',
226 product_description3 => '',
227 product_description4 => ''
231 purchase_order_num => '',
234 alt_tax_amount => '',
235 discount_amount => '',
236 freight_amount => '',
238 line_item_count => '',
239 purchase_items => $self->_parse_line_items()
244 # create the list of required fields based on the action
245 my @required_fields = qw/ amount /;
246 if ($action eq 'charge') {
247 push(@required_fields, $_) foreach (qw/ account_number cvv exp_month exp_year /);
248 }elsif ($action eq 'void') {
249 push(@required_fields, $_) foreach (qw/ reference_number transaction_date /);
250 }elsif ($action eq 'refund') {
251 push(@required_fields, $_) foreach (qw/ amount account_number exp_month exp_year /);
254 # check the requirements are met.
256 foreach my $field (@required_fields) {
257 push(@missing_fields, $field) if (!$template_vars->{payment}{$field});
259 if (scalar(@missing_fields)) {
260 croak "Missing required fields: ".join(', ', @missing_fields);
263 # read in the appropriate xml template
264 my $xml_template = _get_xml_template( $action );
265 # create a template object.
266 my $tt = Template->new();
267 # populate the XML template.
269 $tt->process( \$xml_template, $template_vars, \$xml_data ) || croak $tt->error();
271 warn "XML:\n$xml_data\n" if $DEBUG > 2;
273 my $boundary = sprintf('FormBoundary%06d', int(rand(1000000)));
274 # opts for B:OP:HTTPS::https_post
275 my $opts = { headers => {}};
276 $opts->{'Content-Type'} =
277 $opts->{headers}->{'Content-Type'} =
278 "multipart/form-data, boundary=$boundary";
282 "Content-Disposition: form-data; name=\"param\"\n\n".
286 # conform to RFC standards
287 $content =~ s/\n/\r\n/gs;
289 my ( $page, $server_response, %headers ) = $self->https_post( $opts, $content );
291 # store the server response.
292 $self->server_response($server_response);
293 # parse the result page.
294 $self->parse_response($page);
296 if (!$self->is_success() && !$self->error_message() ) {
298 #additional logging information, possibly too sensitive for an error msg
299 # (vSecureProcessing seems to have a failure mode where they return the full
300 # original request including card number)
301 $self->error_message(
302 "(HTTPS response: ".$server_response.") ".
304 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
305 "(Raw HTTPS content: ".$page.")"
308 my $response_code = $self->response_code() || '';
309 if ($response_code) {
310 $self->error_message(qq|Error code ${response_code} was returned by vSecureProcessing. (enable debugging for raw HTTPS response)|);
312 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
319 # read $self->server_response and decipher any errors
324 if ($self->server_response =~ /^200/) {
325 my $response = XMLin($page);
326 $self->result_code($response->{Status}); # 0 /1
327 $self->response_code($response->{ResponseCode}); # see documentation for translation
328 $self->avs_response($response->{AvsResponse}); # Y / N
329 $self->cvv_response($response->{CvvResponse}); # P / F
330 $self->txn_date($response->{TransactionDate}); # MMDDhhmmss
331 $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100
332 $self->reference_number($response->{ReferenceNumber});
334 $self->is_success($self->result_code() eq '0' ? 1 : 0);
335 if ($self->is_success()) {
336 $self->authorization($response->{AuthIdentificationResponse});
338 # fill in error_message if there is is an error
339 if ( !$self->is_success && exists($response->{AdditionalResponseData})) {
340 $self->error_message('Error '.$response->{ResponseCode}.': '.$response->{AdditionalResponseData});
341 }elsif ( !$self->is_success && exists($response->{Receipt}) ) {
342 $self->error_message('Error '.$response->{ResponseCode}.': '.(exists($response->{Receipt})) ? $response->{Receipt} : '');
346 $self->is_success(0);
347 $self->error_message('Error communicating with vSecureProcessing server');
354 sub _get_xml_template {
357 my $xml_template = q|<Request >
359 <Platform>[% auth.platform %]</Platform>
360 <UserId>[% auth.userid %]</UserId>
361 <GID>[% auth.gid %]</GID>
362 <Tid>[% auth.tid %]</Tid>
366 if ($action eq 'charge') {
367 $xml_template .= _get_xml_template_charge();
368 }elsif($action eq 'void') {
369 $xml_template .= _get_xml_template_void();
370 }elsif($action eq 'authorize') {
371 $xml_template .= _get_xml_template_auth();
372 }elsif($action eq 'authorize_cancel') {
373 $xml_template .= _get_xml_template_auth_cancel();
374 }elsif($action eq 'refund') {
375 $xml_template .= _get_xml_template_refund();
376 }elsif($action eq 'capture') {
377 $xml_template .= _get_xml_template_capture();
378 }elsif($action eq 'create_token') {
379 $xml_template .= _get_xml_template_create_token();
380 }elsif($action eq 'delete_token') {
381 $xml_template .= _get_xml_template_delete_token();
382 }elsif($action eq 'query_token') {
383 $xml_template .= _get_xml_template_query_token();
384 }elsif($action eq 'update_exp_date') {
385 $xml_template .= _get_xml_template_update_exp_date();
386 }elsif($action eq 'update_token') {
387 $xml_template .= _get_xml_template_update_token();
390 $xml_template .= "</Request>";
391 $xml_template =~ s/[\n\t\s]*//g;
393 return $xml_template;
396 sub _get_xml_template_charge {
397 my $xml_template = q|<ProcessPayment>
398 <Amount>[% payment.amount %]</Amount>
399 <Trk1>[% payment.track1 %]</Trk1>
400 <Trk2>[% payment.track2 %]</Trk2>
401 <TypeOfSale>[% payment.type %]</TypeOfSale>
402 <Cf1>[% payment.cf1 %]</Cf1>
403 <Cf2>[% payment.cf2 %]</Cf2>
404 <Cf3>[% payment.cf3 %]</Cf3>
405 <AccountNumber>[% payment.account_number %]</AccountNumber>
406 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
407 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
408 <Cvv>[% payment.cvv %]</Cvv>
409 <CardHolderFirstName>[% payment.first_name %]</CardHolderFirstName>
410 <CardHolderLastName>[% payment.last_name %]</CardHolderLastName>
411 <AvsZip>[% payment.postal_code %]</AvsZip>
412 <AvsStreet>[% payment.street_address %]</AvsStreet>
414 <IndType >[% payment.industry_type %]</IndType >
415 <IndInvoice>[% payment.invoice_num %]</IndInvoice>
417 <ApplicationId>[% payment.appid %]</ApplicationId>
418 <Recurring>[% payment.recurring %]</Recurring>
421 # other options (that we are not using right now):
422 # <Level2PurchaseInfo>
423 # <Level2CardType>[% level2.card_type %]</Level2CardType >
424 # <PurchaseCode>[% level2.purchase_code %]</PurchaseCode>
425 # <ShipToCountryCode>[% level2.country_code %]</ShipToCountryCode>
426 # <ShipToPostalCode>[% level2.ship_tp_postal_code %]</ShipToPostalCode>
427 # <ShipFromPostalCode>[% level2.ship_from_postal_code %]</ShipFromPostalCode>
428 # <SalesTax>[% level2.sales_tax %]</SalesTax>
429 # <ProductDescription1>[% level2.product_description1 %]</ProductDescription1>
430 # <ProductDescription2>[% level2.product_description2 %]</ProductDescription2>
431 # <ProductDescription3>[% level2.product_description3 %]</ProductDescription3>
432 # <ProductDescription4>[% level2.product_description4 %]</ProductDescription4>
433 # </Level2PurchaseInfo>
434 # <Level3PurchaseInfo>
435 # <PurchaseOrderNumber>[% level3.purchase_order_num %]</PurchaseOrderNumber>
436 # <OrderDate>[% level3.order_date %]</OrderDate>
437 # <DutyAmount>[% level3.duty_amount %]</DutyAmount>
438 # <AlternateTaxAmount>[% level3.alt_tax_amount %]</AlternateTaxAmount>
439 # <DiscountAmount>[% level3.discount_amount %]</DiscountAmount>
440 # <FreightAmount>[% level3.freight_amount %]</FreightAmount>
441 # <TaxExemptFlag>[% level3.tax_exempt %]</TaxExemptFlag>
442 # <LineItemCount>[% level3.line_item_count %]</LineItemCount>
444 # [% level3.purchase_items %]
446 # </Level3PurchaseInfo>
448 return $xml_template;
451 sub _parse_line_items {
453 my %content = $self->content();
455 return '' if (!$content{'items'});
458 my $template = q| <LineItem>
459 <ItemSequenceNumber>[% seq_num %]</ItemSequenceNumber>
460 <ItemCode>[% code %]</ItemCode>
461 <ItemDescription>[% desc %]</ItemDescription>
462 <ItemQuantity>[% qty %]</ItemQuantity>
463 <ItemUnitOfMeasure>[% unit %]</ItemUnitOfMeasure>
464 <ItemUnitCost>[% unit_cost %]</ItemUnitCost>
465 <ItemAmount>[% amount %]</ItemAmount>
466 <ItemDiscountAmount>[% discount_amount %]</ItemDiscountAmount>
467 <ItemTaxAmount>[% tax_amount %]</ItemTaxAmount>
468 <ItemTaxRate>[% tax_rate %]</ItemTaxRate>
472 my @items = $content{'items'};
473 foreach my $item (@items) {
474 # fille in the slots from $template with details in $item
475 # push to @line_items
478 return join("\n", @line_items);
481 sub _get_xml_template_void {
482 my $xml_template = q|<ProcessVoid>
483 <Amount>[% payment.amount %]</Amount>
484 <AccountNumber>[% payment.account_number %]</AccountNumber>
485 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
486 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
487 <ReferenceNumber>[% payment.reference_number %]</ReferenceNumber>
489 <IndustryType1>[% payment.industry_type %]</IndustryType1>
490 <ApplicationId>[% payment.appid %]</ApplicationId>
493 return $xml_template;
496 sub _get_xml_template_refund {
497 my $xml_template = q|<ProcessRefund>
498 <Amount>[% payment.amount %]</Amount>
499 <AccountNumber>[% payment.account_number %]</AccountNumber>
500 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
501 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
502 <ApplicationId>[% payment.appid %]</ApplicationId>
505 return $xml_template;
508 sub _get_xml_template_auth {
509 my $xml_template = '';
511 return $xml_template;
514 sub _get_xml_template_auth_cancel {
515 my $xml_template = '';
517 return $xml_template;
520 sub _get_xml_template_capture {
521 my $xml_template = '';
523 return $xml_template;
526 sub _get_xml_template_create_token {
527 my $xml_template = '';
529 return $xml_template;
532 sub _get_xml_template_delete_token {
533 my $xml_template = '';
535 return $xml_template;
538 sub _get_xml_template_query_token {
539 my $xml_template = '';
541 return $xml_template;
544 sub _get_xml_template_update_exp_date {
545 my $xml_template = '';
547 return $xml_template;
550 sub _get_xml_template_update_token {
551 my $xml_template = '';
553 return $xml_template;
563 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
567 use Business::OnlinePayment;
568 my %processor_info = (
570 gid => 12345678901234567890,
573 url => 'www.####.com'
576 new Business::OnlinePayment( "vSecureProcessing", %processor_info);
580 action => 'Normal Authorization',
581 description => 'Business::OnlinePayment test',
583 customer_id => 'tfb',
584 name => 'Tofu Beast',
585 address => '123 Anystreet',
589 card_number => '4007000000027',
590 expiration => '09/02',
591 cvv2 => '1234', #optional
595 if($tx->is_success()) {
596 print "Card processed successfully: ".$tx->authorization."\n";
598 print "Card was rejected: ".$tx->error_message."\n";
603 For detailed information see L<Business::OnlinePayment>.
605 =head1 METHODS AND FUNCTIONS
607 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
611 Returns the response error code.
615 Returns the response error description text.
617 =head2 server_response
619 Returns the complete response from the server.
621 =head1 Handling of content(%content) data:
625 The following actions are valid
631 =head1 Setting vSecureProcessing parameters from content(%content)
633 The following rules are applied to map data to vSecureProcessing parameters
634 from content(%content):
636 # param => $content{<key>}
637 AccountNumber => 'card_number',
639 ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration'
640 ExpirationYear => \( $year ), # YY from MM/YY of 'expiration'
643 CardHolderFirstName => 'first_name',
644 CardHolderLastName => 'last_name',
646 AvsStreet => 'address',
650 IndustryType => 'IndustryInfo',
656 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document Version: 140901 (September 1, 2014).
658 See http://www.vsecureprocessing.com/ for more information.
662 Original author: Alex Brelsfoard
664 Current maintainer: Alex Brelsfoard
668 Copyright (c) 2015 Freeside Internet Services, Inc.
672 This program is free software; you can redistribute it and/or modify it under
673 the same terms as Perl itself.
677 Need a complete, open-source back-office and customer self-service solution?
678 The Freeside software includes support for credit card and electronic check
679 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
680 trouble ticketing, and customer signup and self-service web interfaces.
682 http://freeside.biz/freeside/
686 perl(1). L<Business::OnlinePayment>.