1 package Business::OnlinePayment::vSecureProcessing;
11 use Business::OnlinePayment;
12 use Business::OnlinePayment::HTTPS;
13 use Net::SSLeay qw(post_http post_https make_headers make_form);
14 use vars qw($VERSION $DEBUG @ISA $me);
16 @ISA = qw(Business::OnlinePayment::HTTPS);
19 $me = 'Business::OnlinePayment::vSecureProcessing';
22 # $server: http://dvrotsos2.kattare.com
24 # mapping out all possible endpoints
25 # but this version will only be building out "charge", "void", & "credit"
26 my %payment_actions = (
28 path => '/vsg2/processpayment',
31 path => '/vsg2/processvoid',
34 path => '/vsg2/processrefund',
37 path => '/vsg2/processauth',
39 'authorize_cancel' => {
40 path => '/vsg2/processauthcancel',
43 path => '/vsg2/processcaptureonly',
46 path => '/vsg2/createtoken',
49 path => '/vsg2/deletetoken',
52 path => '/vsg2/querytoken',
54 'update_exp_date' => {
55 path => '/vsg2/updateexpiration',
58 path => '/vsg2/updatetoken',
63 my %action_mapping = (
64 'normal authorization' => 'charge',
66 'authorization only' => 'authorize',
67 'post authorization' => 'capture',
68 'reverse authorization' => 'authorize_cancel'
73 eval 'use bytes; sub blength ($) { length $_[0] }';
74 $@ and eval ' sub blength ($) { length $_[0] }' ;
76 sub Net::SSLeay::do_httpx3 {
77 my ($method, $usessl, $site, $port, $path, $headers,
78 $content, $mime_type, $crt_path, $key_path) = @_;
79 my ($response, $page, $h,$v);
80 my $CRLF = "\x0d\x0a"; # because \r\n is not fully portable
82 $mime_type = "";#application/x-www-form-urlencoded" unless $mime_type;
83 my $len = blength($content);
84 #$content = "$mime_type${CRLF}Content-Length: $len$CRLF$CRLF$content";
85 $content = "Cache-Control: no-cache$CRLF"
86 . "Content-Type: multipart/form-data; boundary=----FormBoundaryE19zNvXGzXaLvS5C$CRLF"
88 . "Content-Length: $len$CRLF$CRLF$content";
90 $content = "$CRLF$CRLF";
92 my $req = "$method $path HTTP/1.1$CRLF";
93 unless (defined $headers && $headers =~ /^Host:/m) {
94 $req .= "Host: $site";
95 unless (($port == 80 && !$usessl) || ($port == 443 && $usessl)) {
100 $req .= (defined $headers ? $headers : '') . "$content";
102 warn "do_httpx3($method,$usessl,$site:$port)" if $DEBUG;
103 my ($http, $errs, $server_cert)
104 = Net::SSLeay::httpx_cat($usessl, $site, $port, $req, $crt_path, $key_path);
105 return (undef, "HTTP/1.0 900 NET OR SSL ERROR$CRLF$CRLF$errs") if $errs;
107 $http = '' if !defined $http;
108 ($headers, $page) = split /\s?\n\s?\n/, $http, 2;
109 warn "headers >$headers< page >>$page<< http >>>$http<<<" if $DEBUG>1;
110 ($response, $headers) = split /\s?\n/, $headers, 2;
111 return ($page, $response, $headers, $server_cert);
114 sub Net::SSLeay::make_form {
118 my ($name, $data) = (shift(@fields), shift(@fields));
119 # $data =~ s/([^\w\-.\@\$ ])/sprintf("%%%2.2x",ord($1))/gse;
121 $form .= "$name=$data&";
131 # inistialize standard B::OP attributes
132 $self->is_success(0);
133 $self->$_( '' ) for qw/authorization
141 # B::OP creates the following accessors:
142 # server, port, path, test_transaction, transaction_type,
143 # server_response, is_success, authorization,
144 # result_code, error_message,
146 $self->build_subs(qw/
147 env platform userid gid tid appid action
148 cvv_response avs_response risk_score
151 $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
155 $self->server($options{'url'});
157 $self->gid($options{'gid'});
159 $self->tid($options{'tid'});
161 $self->platform($options{'platform'});
163 $self->appid($options{'appid'});
165 $self->env((defined($options{'env'})) ? $options{'env'} : 'live'); # 'live'/'test'
167 # $self->port(($options{'env'} eq 'test') ? 80 : 443);
174 my ($self,$content) = @_;
175 my %content = $self->content();
178 no warnings 'uninitialized';
180 # strip non-digits from card number
181 my $card_number = '';
182 if ( $content{card_number} ) {
183 $content{card_number} =~ s/\D//g;
186 # separate month and year values for expiry_date
187 if ( $content{expiration} ) {
188 ($content{exp_month}, $content{exp_year}) = split /\//, $content{expiration};
189 $content{exp_month} = sprintf "%02d", $content{exp_month};
190 $content{exp_year} = substr($content{exp_year},0,2) if ($content{exp_year} > 99);
193 if (!$content{'first_name'} || !$content{'last_name'} && $content{'name'}) {
194 ($content{'first_name'}, $content{'last_name'}) = split(' ', $content{'name'}, 2);
197 if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
198 $content{'street_number'} = $1;
201 warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
202 $self->content(%content);
205 sub process_content {
207 $self->clean_content();
208 my %content = $self->content();
209 $self->action(($action_mapping{lc $content{'action'}}) ? $action_mapping{lc $content{'action'}} : lc $content{'action'});
210 $self->path($payment_actions{ $self->action }{path});
211 $self->appid($content{appid}) if (!$self->appid && $content{appid});
217 # inistialize standard B::OP attributes
218 $self->is_success(0);
219 $self->$_( '' ) for qw/authorization
224 # clean and process the $self->content info
225 $self->process_content();
226 my %content = $self->content;
227 my $action = $self->action();
229 my @acceptable_actions = ('charge', 'refund', 'void');
231 unless ( grep { $action eq $_ } @acceptable_actions ) {
232 croak "'$action' is not supported at this time.";
235 # fill out the template vars
236 my $template_vars = {
239 platform => $self->platform,
240 userid => $self->userid,
246 amount => $content{'amount'},
247 track1 => ($content{'track1'}) ? $content{'track1'} : '',
248 track2 => ($content{'track2'}) ? $content{'track2'} : '',
249 type => ($content{'description'}) ? $content{'description'} : '',
250 cf1 => ($content{'UDField1'}) ? $content{'UDField1'} : '',
251 cf2 => ($content{'UDField2'}) ? $content{'UDField2'} : '',
253 account_number => ($content{'card_number'}) ? $content{'card_number'} : '',
254 exp_month => $content{'exp_month'},
255 exp_year => $content{'exp_year'},
256 cvv => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
257 first_name => ($content{'first_name'}) ? $content{'first_name'} : '',
258 last_name => ($content{'last_name'}) ? $content{'last_name'} : '',
259 postal_code => ($content{'zip'}) ? $content{'zip'} : '',
260 street_address => ($content{'street_number'}) ? $content{'street_number'} : '',
261 industry_type => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
262 invoice_num => ($content{'invoice_number'}) ? $content{'invoice_number'} : '',
263 appid => $self->appid(),
264 recurring => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
265 response_code => ($content{'response_code'}) ? $content{'response_code'} : '',
266 reference_number=> ($content{'ref_num'}) ? $content{'ref_num'} : '',
267 token => ($content{'token'}) ? $content{'token'} : '',
268 receipt => ($content{'receipt'}) ? $content{'receipt'} : '',
269 transaction_date=> ($content{'txn_date'}) ? $content{'txn_date'} : '',
270 merchant_data => ($content{'merchant_data'}) ? $content{'merchant_data'} : '',
273 # we won't be using level2 nor level3. So I'm leaving them blank for now.
278 ship_tp_postal_code => '',
279 ship_from_postal_code => '',
281 product_description1 => '',
282 product_description2 => '',
283 product_description3 => '',
284 product_description4 => ''
288 purchase_order_num => '',
291 alt_tax_amount => '',
292 discount_amount => '',
293 freight_amount => '',
295 line_item_count => '',
296 purchase_items => $self->_parse_line_items()
301 # create the list of required fields based on the action
302 my @required_fields = qw/ amount /;
303 if ($action eq 'charge') {
304 push(@required_fields, $_) foreach (qw/ account_number cvv exp_month exp_year /);
305 }elsif ($action eq 'void') {
306 push(@required_fields, $_) foreach (qw/ response_code reference_number receipt token transaction_date exp_month exp_year /);
307 }elsif ($action eq 'refund') {
308 push(@required_fields, $_) foreach (qw/ merchant_data token account_number exp_month exp_year /);
311 # check the requirements are met.
313 foreach my $field (@required_fields) {
314 push(@missing_fields, $field) if (!$template_vars->{payment}{$field});
316 if (scalar(@missing_fields)) {
317 croak "Missing required fields: ".join(', ', @missing_fields);
320 # read in the appropriate xml template
321 my $xml_template .= _get_xml_template( $action );
322 # create a template object.
323 my $tt = Template->new();
324 # populate the XML template.
326 $tt->process( \$xml_template, $template_vars, \$xml_data ) || croak $tt->error();
328 warn "XML:\n$xml_data\n" if $DEBUG > 2;
330 # my $opts = {'Content-Type' => 'multipart/form-data'};
331 my $opts = {'Cache-Control' => 'no-cache', 'Content-Type' => 'multipart/form-data; boundary=----FormBoundaryE19zNvXGzXaLvS5C'};
332 my $params = {param => $xml_data};
333 my $content = qq|----FormBoundaryE19zNvXGzXaLvS5C
334 Content-Disposition: form-data; name="param"
337 ----FormBoundaryE19zNvXGzXaLvS5C|;
338 my ( $page, $server_response, %headers ) = $self->https_post( $opts, $content );
340 # store the server response.
341 $self->server_response($server_response);
342 # parse the result page.
343 $self->parse_response($page);
345 if (!$self->is_success() && !$self->error_message() ) {
347 #additional logging information, possibly too sensitive for an error msg
348 # (vSecureProcessing seems to have a failure mode where they return the full
349 # original request including card number)
350 $self->error_message(
351 "(HTTPS response: ".$server_response.") ".
353 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
354 "(Raw HTTPS content: ".$page.")"
357 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
363 # read $self->server_response and decipher any errors
368 if ($self->server_response =~ /^200/) {
369 my $response = XMLin($page);
370 $self->result_code($response->{Status});
371 $self->avs_response($response->{AvsResponse});
372 $self->cvv_response($response->{CvvResponse});
373 $self->is_success($self->result_code() eq '0' ? 1 : 0);
374 if ($self->is_success()) {
375 $self->authorization($response->{AuthIdentificationResponse});
377 # fill in error_message if there is is an error
378 if ( !$self->is_success && exists($response->{ResultCode})) {
379 $self->error_message('Error '.$response->{ResponseCode}.': '.$response->{ResultCode});
380 }elsif ( !$self->is_success && exists($response->{Receipt}) ) {
381 $self->error_message('Error '.$response->{ResponseCode}.': '.(exists($response->{Receipt})) ? $response->{Receipt} : '');
385 $self->is_success(0);
386 $self->error_message('Error communicating with vSecureProcessing server');
393 sub _get_xml_template {
396 my $xml_template = q|<Request >
398 <Platform>[% auth.platform %]</Platform>
399 <UserId>[% auth.userid %]</UserId>
400 <GID>[% auth.gid %]</GID>
401 <Tid>[% auth.tid %]</Tid>
405 if ($action eq 'charge') {
406 $xml_template .= _get_xml_template_charge();
407 }elsif($action eq 'void') {
408 $xml_template .= _get_xml_template_void();
409 }elsif($action eq 'authorize') {
410 $xml_template .= _get_xml_template_auth();
411 }elsif($action eq 'authorize_cancel') {
412 $xml_template .= _get_xml_template_auth_cancel();
413 }elsif($action eq 'refund') {
414 $xml_template .= _get_xml_template_refund();
415 }elsif($action eq 'capture') {
416 $xml_template .= _get_xml_template_capture();
417 }elsif($action eq 'create_token') {
418 $xml_template .= _get_xml_template_create_token();
419 }elsif($action eq 'delete_token') {
420 $xml_template .= _get_xml_template_delete_token();
421 }elsif($action eq 'query_token') {
422 $xml_template .= _get_xml_template_query_token();
423 }elsif($action eq 'update_exp_date') {
424 $xml_template .= _get_xml_template_update_exp_date();
425 }elsif($action eq 'update_token') {
426 $xml_template .= _get_xml_template_update_token();
429 $xml_template .= "</Request>";
430 $xml_template =~ s/[\n\t\s]*//g;
432 return $xml_template;
435 sub _get_xml_template_charge {
436 my $xml_template = q|<ProcessPayment>
437 <Amount>[% payment.amount %]</Amount>
438 <Trk1>[% payment.track1 %]</Trk1>
439 <Trk2>[% payment.track2 %]</Trk2>
440 <TypeOfSale>[% payment.type %]</TypeOfSale>
441 <Cf1>[% payment.cf1 %]</Cf1>
442 <Cf2>[% payment.cf2 %]</Cf2>
443 <Cf3>[% payment.cf3 %]</Cf3>
444 <AccountNumber>[% payment.account_number %]</AccountNumber>
445 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
446 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
447 <Cvv>[% payment.cvv %]</Cvv>
448 <CardHolderFirstName>[% payment.first_name %]</CardHolderFirstName>
449 <CardHolderLastName>[% payment.last_name %]</CardHolderLastName>
450 <AvsZip>[% payment.postal_code %]</AvsZip>
451 <AvsStreet>[% payment.street_address %]</AvsStreet>
453 <IndType >[% payment.industry_type %]</IndType >
454 <IndInvoice>[% payment.invoice_num %]</IndInvoice>
456 <ApplicationId>[% payment.appid %]</ApplicationId>
457 <Recurring>[% payment.recurring %]</Recurring>
460 # other options (that we are not using right now):
461 # <Level2PurchaseInfo>
462 # <Level2CardType>[% level2.card_type %]</Level2CardType >
463 # <PurchaseCode>[% level2.purchase_code %]</PurchaseCode>
464 # <ShipToCountryCode>[% level2.country_code %]</ShipToCountryCode>
465 # <ShipToPostalCode>[% level2.ship_tp_postal_code %]</ShipToPostalCode>
466 # <ShipFromPostalCode>[% level2.ship_from_postal_code %]</ShipFromPostalCode>
467 # <SalesTax>[% level2.sales_tax %]</SalesTax>
468 # <ProductDescription1>[% level2.product_description1 %]</ProductDescription1>
469 # <ProductDescription2>[% level2.product_description2 %]</ProductDescription2>
470 # <ProductDescription3>[% level2.product_description3 %]</ProductDescription3>
471 # <ProductDescription4>[% level2.product_description4 %]</ProductDescription4>
472 # </Level2PurchaseInfo>
473 # <Level3PurchaseInfo>
474 # <PurchaseOrderNumber>[% level3.purchase_order_num %]</PurchaseOrderNumber>
475 # <OrderDate>[% level3.order_date %]</OrderDate>
476 # <DutyAmount>[% level3.duty_amount %]</DutyAmount>
477 # <AlternateTaxAmount>[% level3.alt_tax_amount %]</AlternateTaxAmount>
478 # <DiscountAmount>[% level3.discount_amount %]</DiscountAmount>
479 # <FreightAmount>[% level3.freight_amount %]</FreightAmount>
480 # <TaxExemptFlag>[% level3.tax_exempt %]</TaxExemptFlag>
481 # <LineItemCount>[% level3.line_item_count %]</LineItemCount>
483 # [% level3.purchase_items %]
485 # </Level3PurchaseInfo>
487 return $xml_template;
490 sub _parse_line_items {
492 my %content = $self->content();
494 return '' if (!$content{'items'});
497 my $template = q| <LineItem>
498 <ItemSequenceNumber>[% seq_num %]</ItemSequenceNumber>
499 <ItemCode>[% code %]</ItemCode>
500 <ItemDescription>[% desc %]</ItemDescription>
501 <ItemQuantity>[% qty %]</ItemQuantity>
502 <ItemUnitOfMeasure>[% unit %]</ItemUnitOfMeasure>
503 <ItemUnitCost>[% unit_cost %]</ItemUnitCost>
504 <ItemAmount>[% amount %]</ItemAmount>
505 <ItemDiscountAmount>[% discount_amount %]</ItemDiscountAmount>
506 <ItemTaxAmount>[% tax_amount %]</ItemTaxAmount>
507 <ItemTaxRate>[% tax_rate %]</ItemTaxRate>
511 my @items = $content{'items'};
512 foreach my $item (@items) {
513 # fille in the slots from $template with details in $item
514 # push to @line_items
517 return join("\n", @line_items);
520 sub _get_xml_template_void {
521 my $xml_template = q|<ProcessVoid>
522 <Amount>[% payment.amount %]</Amount>
523 <AccountNumber>[% payment.account_number %]</AccountNumber>
524 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
525 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
526 <ReferenceNumber>[% payment.reference_number %]</ReferenceNumber>
528 <IndustryType1>[% payment.industry_type %]</IndustryType1>
529 <ApplicationId>[% payment.appid %]</ApplicationId>
532 return $xml_template;
535 sub _get_xml_template_refund {
536 my $xml_template = q|<ProcessRefund>
537 <Amount>[% payment.amount %]</Amount>
538 <AccountNumber>[% payment.account_number %]</AccountNumber>
539 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
540 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
541 <ApplicationId>[% payment.appid %]</ApplicationId>
544 return $xml_template;
547 sub _get_xml_template_auth {
548 my $xml_template = '';
550 return $xml_template;
553 sub _get_xml_template_auth_cancel {
554 my $xml_template = '';
556 return $xml_template;
559 sub _get_xml_template_capture {
560 my $xml_template = '';
562 return $xml_template;
565 sub _get_xml_template_create_token {
566 my $xml_template = '';
568 return $xml_template;
571 sub _get_xml_template_delete_token {
572 my $xml_template = '';
574 return $xml_template;
577 sub _get_xml_template_query_token {
578 my $xml_template = '';
580 return $xml_template;
583 sub _get_xml_template_update_exp_date {
584 my $xml_template = '';
586 return $xml_template;
589 sub _get_xml_template_update_token {
590 my $xml_template = '';
592 return $xml_template;
602 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
606 use Business::OnlinePayment;
607 my %processor_info = (
609 gid => 12345678901234567890,
612 url => 'www.####.com'
615 new Business::OnlinePayment( "vSecureProcessing", %processor_info);
619 action => 'Normal Authorization',
620 description => 'Business::OnlinePayment test',
622 customer_id => 'tfb',
623 name => 'Tofu Beast',
624 address => '123 Anystreet',
628 card_number => '4007000000027',
629 expiration => '09/02',
630 cvv2 => '1234', #optional
634 if($tx->is_success()) {
635 print "Card processed successfully: ".$tx->authorization."\n";
637 print "Card was rejected: ".$tx->error_message."\n";
642 For detailed information see L<Business::OnlinePayment>.
644 =head1 METHODS AND FUNCTIONS
646 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
650 Returns the response error code.
654 Returns the response error description text.
656 =head2 server_response
658 Returns the complete response from the server.
660 =head1 Handling of content(%content) data:
664 The following actions are valid
670 =head1 Setting vSecureProcessing parameters from content(%content)
672 The following rules are applied to map data to vSecureProcessing parameters
673 from content(%content):
675 # param => $content{<key>}
676 AccountNumber => 'card_number',
678 ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration'
679 ExpirationYear => \( $year ), # YY from MM/YY of 'expiration'
682 CardHolderFirstName => 'first_name',
683 CardHolderLastName => 'last_name',
685 AvsStreet => 'address',
689 IndustryType => 'IndustryInfo',
695 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document Version: 140901 (September 1, 2014).
697 See http://www.vsecureprocessing.com/ for more information.
701 Original author: Alex Brelsfoard
703 Current maintainer: Alex Brelsfoard
707 Copyright (c) 2015 Freeside Internet Services, Inc.
711 This program is free software; you can redistribute it and/or modify it under
712 the same terms as Perl itself.
716 Need a complete, open-source back-office and customer self-service solution?
717 The Freeside software includes support for credit card and electronic check
718 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
719 trouble ticketing, and customer signup and self-service web interfaces.
721 http://freeside.biz/freeside/
725 perl(1). L<Business::OnlinePayment>.