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 {
79 # my ($method, $usessl, $site, $port, $path, $headers,
80 # $content, $mime_type, $crt_path, $key_path) = @_;
81 # my ($response, $page, $h,$v);
82 # my $CRLF = "\x0d\x0a"; # because \r\n is not fully portable
84 # $mime_type = "";#application/x-www-form-urlencoded" unless $mime_type;
85 # my $len = length($content);
86 # #$content = "$mime_type${CRLF}Content-Length: $len$CRLF$CRLF$content";
87 # $content = "Cache-Control: no-cache$CRLF"
88 # . "Content-Type: multipart/form-data; boundary=--FormBoundaryE19zNvXGzXaLvS5C$CRLF"
89 # . "Accept: */*$CRLF"
90 # . "Content-Length: $len$CRLF$CRLF$content";
92 # $content = "$CRLF$CRLF";
94 # my $req = "$method $path HTTP/1.1$CRLF";
95 # unless (defined $headers && $headers =~ /^Host:/m) {
96 # $req .= "Host: $site";
97 # unless (($port == 80 && !$usessl) || ($port == 443 && $usessl)) {
102 # $req .= (defined $headers ? $headers : '') . "$content";
104 # warn "do_httpx3($method,$usessl,$site:$port)" if $DEBUG;
105 # my ($http, $errs, $server_cert)
106 # = Net::SSLeay::httpx_cat($usessl, $site, $port, $req, $crt_path, $key_path);
107 # return (undef, "HTTP/1.0 900 NET OR SSL ERROR$CRLF$CRLF$errs") if $errs;
109 # $http = '' if !defined $http;
110 # ($headers, $page) = split /\s?\n\s?\n/, $http, 2;
111 # warn "headers >$headers< page >>$page<< http >>>$http<<<" if $DEBUG>1;
112 # ($response, $headers) = split /\s?\n/, $headers, 2;
113 # return ($page, $response, $headers, $server_cert);
116 #sub Net::SSLeay::make_form {
120 # my ($name, $data) = (shift(@fields), shift(@fields));
121 ## $data =~ s/([^\w\-.\@\$ ])/sprintf("%%%2.2x",ord($1))/gse;
122 ## $data =~ tr[ ][+];
123 # $form .= "$name=$data&";
133 # inistialize standard B::OP attributes
134 $self->is_success(0);
135 $self->$_( '' ) for qw/authorization
143 # B::OP creates the following accessors:
144 # server, port, path, test_transaction, transaction_type,
145 # server_response, is_success, authorization,
146 # result_code, error_message,
148 $self->build_subs(qw/
149 env platform userid gid tid appid action
150 cvv_response avs_response risk_score
153 $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
157 $self->server($options{'server'});
159 $self->gid($options{'gid'});
161 $self->tid($options{'tid'});
163 $self->platform($options{'platform'});
165 $self->appid($options{'appid'});
167 $self->env((defined($options{'env'})) ? $options{'env'} : 'live'); # 'live'/'test'
169 # $self->port(($options{'env'} eq 'test') ? 80 : 443);
176 my ($self,$content) = @_;
177 my %content = $self->content();
180 no warnings 'uninitialized';
182 # strip non-digits from card number
183 my $card_number = '';
184 if ( $content{card_number} ) {
185 $content{card_number} =~ s/\D//g;
188 # separate month and year values for expiry_date
189 if ( $content{expiration} ) {
190 ($content{exp_month}, $content{exp_year}) = split /\//, $content{expiration};
191 $content{exp_month} = sprintf "%02d", $content{exp_month};
192 $content{exp_year} = substr($content{exp_year},0,2) if ($content{exp_year} > 99);
195 if (!$content{'first_name'} || !$content{'last_name'} && $content{'name'}) {
196 ($content{'first_name'}, $content{'last_name'}) = split(' ', $content{'name'}, 2);
199 if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
200 $content{'street_number'} = $1;
203 warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
204 $self->content(%content);
207 sub process_content {
209 $self->clean_content();
210 my %content = $self->content();
211 $self->action(($action_mapping{lc $content{'action'}}) ? $action_mapping{lc $content{'action'}} : lc $content{'action'});
212 $self->path($payment_actions{ $self->action }{path})
213 unless length($self->path);
214 $self->appid($content{appid}) if (!$self->appid && $content{appid});
220 # inistialize standard B::OP attributes
221 $self->is_success(0);
222 $self->$_( '' ) for qw/authorization
227 # clean and process the $self->content info
228 $self->process_content();
229 my %content = $self->content;
230 my $action = $self->action();
232 my @acceptable_actions = ('charge', 'refund', 'void');
234 unless ( grep { $action eq $_ } @acceptable_actions ) {
235 croak "'$action' is not supported at this time.";
238 # fill out the template vars
239 my $template_vars = {
242 platform => $self->platform,
243 userid => $self->userid,
249 amount => $content{'amount'},
250 track1 => ($content{'track1'}) ? $content{'track1'} : '',
251 track2 => ($content{'track2'}) ? $content{'track2'} : '',
252 type => ($content{'description'}) ? $content{'description'} : '',
253 cf1 => ($content{'UDField1'}) ? $content{'UDField1'} : '',
254 cf2 => ($content{'UDField2'}) ? $content{'UDField2'} : '',
256 account_number => ($content{'card_number'}) ? $content{'card_number'} : '',
257 exp_month => $content{'exp_month'},
258 exp_year => $content{'exp_year'},
259 cvv => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
260 first_name => ($content{'first_name'}) ? $content{'first_name'} : '',
261 last_name => ($content{'last_name'}) ? $content{'last_name'} : '',
262 postal_code => ($content{'zip'}) ? $content{'zip'} : '',
263 street_address => ($content{'street_number'}) ? $content{'street_number'} : '',
264 industry_type => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
265 invoice_num => ($content{'invoice_number'}) ? $content{'invoice_number'} : '',
266 appid => $self->appid(),
267 recurring => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
268 response_code => ($content{'response_code'}) ? $content{'response_code'} : '',
269 reference_number=> ($content{'ref_num'}) ? $content{'ref_num'} : '',
270 token => ($content{'token'}) ? $content{'token'} : '',
271 receipt => ($content{'receipt'}) ? $content{'receipt'} : '',
272 transaction_date=> ($content{'txn_date'}) ? $content{'txn_date'} : '',
273 merchant_data => ($content{'merchant_data'}) ? $content{'merchant_data'} : '',
276 # we won't be using level2 nor level3. So I'm leaving them blank for now.
281 ship_tp_postal_code => '',
282 ship_from_postal_code => '',
284 product_description1 => '',
285 product_description2 => '',
286 product_description3 => '',
287 product_description4 => ''
291 purchase_order_num => '',
294 alt_tax_amount => '',
295 discount_amount => '',
296 freight_amount => '',
298 line_item_count => '',
299 purchase_items => $self->_parse_line_items()
304 # create the list of required fields based on the action
305 my @required_fields = qw/ amount /;
306 if ($action eq 'charge') {
307 push(@required_fields, $_) foreach (qw/ account_number cvv exp_month exp_year /);
308 }elsif ($action eq 'void') {
309 push(@required_fields, $_) foreach (qw/ response_code reference_number receipt token transaction_date exp_month exp_year /);
310 }elsif ($action eq 'refund') {
311 push(@required_fields, $_) foreach (qw/ merchant_data token account_number exp_month exp_year /);
314 # check the requirements are met.
316 foreach my $field (@required_fields) {
317 push(@missing_fields, $field) if (!$template_vars->{payment}{$field});
319 if (scalar(@missing_fields)) {
320 croak "Missing required fields: ".join(', ', @missing_fields);
323 # read in the appropriate xml template
324 my $xml_template = _get_xml_template( $action );
325 # create a template object.
326 my $tt = Template->new();
327 # populate the XML template.
329 $tt->process( \$xml_template, $template_vars, \$xml_data ) || croak $tt->error();
331 warn "XML:\n$xml_data\n" if $DEBUG > 2;
333 my $boundary = sprintf('FormBoundary%06d', int(rand(1000000)));
334 # opts for B:OP:HTTPS::https_post
335 my $opts = { headers => {}, debug => $DEBUG };
336 $opts->{'Content-Type'} =
337 $opts->{headers}->{'Content-Type'} =
338 "multipart/form-data, boundary=$boundary";
342 "Content-Disposition: form-data; name=\"param\"\n\n".
346 # conform to RFC standards
347 $content =~ s/\n/\r\n/gs;
349 my ( $page, $server_response, %headers ) = $self->https_post( $opts, $content );
351 # store the server response.
352 $self->server_response($server_response);
353 # parse the result page.
354 $self->parse_response($page);
356 if (!$self->is_success() && !$self->error_message() ) {
358 #additional logging information, possibly too sensitive for an error msg
359 # (vSecureProcessing seems to have a failure mode where they return the full
360 # original request including card number)
361 $self->error_message(
362 "(HTTPS response: ".$server_response.") ".
364 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
365 "(Raw HTTPS content: ".$page.")"
368 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
374 # read $self->server_response and decipher any errors
379 if ($self->server_response =~ /^200/) {
380 my $response = XMLin($page);
381 $self->result_code($response->{Status});
382 $self->avs_response($response->{AvsResponse});
383 $self->cvv_response($response->{CvvResponse});
384 $self->is_success($self->result_code() eq '0' ? 1 : 0);
385 if ($self->is_success()) {
386 $self->authorization($response->{AuthIdentificationResponse});
388 # fill in error_message if there is is an error
389 if ( !$self->is_success && exists($response->{ResultCode})) {
390 $self->error_message('Error '.$response->{ResponseCode}.': '.$response->{ResultCode});
391 }elsif ( !$self->is_success && exists($response->{Receipt}) ) {
392 $self->error_message('Error '.$response->{ResponseCode}.': '.(exists($response->{Receipt})) ? $response->{Receipt} : '');
396 $self->is_success(0);
397 $self->error_message('Error communicating with vSecureProcessing server');
404 sub _get_xml_template {
407 my $xml_template = q|<Request >
409 <Platform>[% auth.platform %]</Platform>
410 <UserId>[% auth.userid %]</UserId>
411 <GID>[% auth.gid %]</GID>
412 <Tid>[% auth.tid %]</Tid>
416 if ($action eq 'charge') {
417 $xml_template .= _get_xml_template_charge();
418 }elsif($action eq 'void') {
419 $xml_template .= _get_xml_template_void();
420 }elsif($action eq 'authorize') {
421 $xml_template .= _get_xml_template_auth();
422 }elsif($action eq 'authorize_cancel') {
423 $xml_template .= _get_xml_template_auth_cancel();
424 }elsif($action eq 'refund') {
425 $xml_template .= _get_xml_template_refund();
426 }elsif($action eq 'capture') {
427 $xml_template .= _get_xml_template_capture();
428 }elsif($action eq 'create_token') {
429 $xml_template .= _get_xml_template_create_token();
430 }elsif($action eq 'delete_token') {
431 $xml_template .= _get_xml_template_delete_token();
432 }elsif($action eq 'query_token') {
433 $xml_template .= _get_xml_template_query_token();
434 }elsif($action eq 'update_exp_date') {
435 $xml_template .= _get_xml_template_update_exp_date();
436 }elsif($action eq 'update_token') {
437 $xml_template .= _get_xml_template_update_token();
440 $xml_template .= "</Request>";
441 $xml_template =~ s/[\n\t\s]*//g;
443 return $xml_template;
446 sub _get_xml_template_charge {
447 my $xml_template = q|<ProcessPayment>
448 <Amount>[% payment.amount %]</Amount>
449 <Trk1>[% payment.track1 %]</Trk1>
450 <Trk2>[% payment.track2 %]</Trk2>
451 <TypeOfSale>[% payment.type %]</TypeOfSale>
452 <Cf1>[% payment.cf1 %]</Cf1>
453 <Cf2>[% payment.cf2 %]</Cf2>
454 <Cf3>[% payment.cf3 %]</Cf3>
455 <AccountNumber>[% payment.account_number %]</AccountNumber>
456 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
457 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
458 <Cvv>[% payment.cvv %]</Cvv>
459 <CardHolderFirstName>[% payment.first_name %]</CardHolderFirstName>
460 <CardHolderLastName>[% payment.last_name %]</CardHolderLastName>
461 <AvsZip>[% payment.postal_code %]</AvsZip>
462 <AvsStreet>[% payment.street_address %]</AvsStreet>
464 <IndType >[% payment.industry_type %]</IndType >
465 <IndInvoice>[% payment.invoice_num %]</IndInvoice>
467 <ApplicationId>[% payment.appid %]</ApplicationId>
468 <Recurring>[% payment.recurring %]</Recurring>
471 # other options (that we are not using right now):
472 # <Level2PurchaseInfo>
473 # <Level2CardType>[% level2.card_type %]</Level2CardType >
474 # <PurchaseCode>[% level2.purchase_code %]</PurchaseCode>
475 # <ShipToCountryCode>[% level2.country_code %]</ShipToCountryCode>
476 # <ShipToPostalCode>[% level2.ship_tp_postal_code %]</ShipToPostalCode>
477 # <ShipFromPostalCode>[% level2.ship_from_postal_code %]</ShipFromPostalCode>
478 # <SalesTax>[% level2.sales_tax %]</SalesTax>
479 # <ProductDescription1>[% level2.product_description1 %]</ProductDescription1>
480 # <ProductDescription2>[% level2.product_description2 %]</ProductDescription2>
481 # <ProductDescription3>[% level2.product_description3 %]</ProductDescription3>
482 # <ProductDescription4>[% level2.product_description4 %]</ProductDescription4>
483 # </Level2PurchaseInfo>
484 # <Level3PurchaseInfo>
485 # <PurchaseOrderNumber>[% level3.purchase_order_num %]</PurchaseOrderNumber>
486 # <OrderDate>[% level3.order_date %]</OrderDate>
487 # <DutyAmount>[% level3.duty_amount %]</DutyAmount>
488 # <AlternateTaxAmount>[% level3.alt_tax_amount %]</AlternateTaxAmount>
489 # <DiscountAmount>[% level3.discount_amount %]</DiscountAmount>
490 # <FreightAmount>[% level3.freight_amount %]</FreightAmount>
491 # <TaxExemptFlag>[% level3.tax_exempt %]</TaxExemptFlag>
492 # <LineItemCount>[% level3.line_item_count %]</LineItemCount>
494 # [% level3.purchase_items %]
496 # </Level3PurchaseInfo>
498 return $xml_template;
501 sub _parse_line_items {
503 my %content = $self->content();
505 return '' if (!$content{'items'});
508 my $template = q| <LineItem>
509 <ItemSequenceNumber>[% seq_num %]</ItemSequenceNumber>
510 <ItemCode>[% code %]</ItemCode>
511 <ItemDescription>[% desc %]</ItemDescription>
512 <ItemQuantity>[% qty %]</ItemQuantity>
513 <ItemUnitOfMeasure>[% unit %]</ItemUnitOfMeasure>
514 <ItemUnitCost>[% unit_cost %]</ItemUnitCost>
515 <ItemAmount>[% amount %]</ItemAmount>
516 <ItemDiscountAmount>[% discount_amount %]</ItemDiscountAmount>
517 <ItemTaxAmount>[% tax_amount %]</ItemTaxAmount>
518 <ItemTaxRate>[% tax_rate %]</ItemTaxRate>
522 my @items = $content{'items'};
523 foreach my $item (@items) {
524 # fille in the slots from $template with details in $item
525 # push to @line_items
528 return join("\n", @line_items);
531 sub _get_xml_template_void {
532 my $xml_template = q|<ProcessVoid>
533 <Amount>[% payment.amount %]</Amount>
534 <AccountNumber>[% payment.account_number %]</AccountNumber>
535 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
536 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
537 <ReferenceNumber>[% payment.reference_number %]</ReferenceNumber>
539 <IndustryType1>[% payment.industry_type %]</IndustryType1>
540 <ApplicationId>[% payment.appid %]</ApplicationId>
543 return $xml_template;
546 sub _get_xml_template_refund {
547 my $xml_template = q|<ProcessRefund>
548 <Amount>[% payment.amount %]</Amount>
549 <AccountNumber>[% payment.account_number %]</AccountNumber>
550 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
551 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
552 <ApplicationId>[% payment.appid %]</ApplicationId>
555 return $xml_template;
558 sub _get_xml_template_auth {
559 my $xml_template = '';
561 return $xml_template;
564 sub _get_xml_template_auth_cancel {
565 my $xml_template = '';
567 return $xml_template;
570 sub _get_xml_template_capture {
571 my $xml_template = '';
573 return $xml_template;
576 sub _get_xml_template_create_token {
577 my $xml_template = '';
579 return $xml_template;
582 sub _get_xml_template_delete_token {
583 my $xml_template = '';
585 return $xml_template;
588 sub _get_xml_template_query_token {
589 my $xml_template = '';
591 return $xml_template;
594 sub _get_xml_template_update_exp_date {
595 my $xml_template = '';
597 return $xml_template;
600 sub _get_xml_template_update_token {
601 my $xml_template = '';
603 return $xml_template;
613 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
617 use Business::OnlinePayment;
618 my %processor_info = (
620 gid => 12345678901234567890,
623 url => 'www.####.com'
626 new Business::OnlinePayment( "vSecureProcessing", %processor_info);
630 action => 'Normal Authorization',
631 description => 'Business::OnlinePayment test',
633 customer_id => 'tfb',
634 name => 'Tofu Beast',
635 address => '123 Anystreet',
639 card_number => '4007000000027',
640 expiration => '09/02',
641 cvv2 => '1234', #optional
645 if($tx->is_success()) {
646 print "Card processed successfully: ".$tx->authorization."\n";
648 print "Card was rejected: ".$tx->error_message."\n";
653 For detailed information see L<Business::OnlinePayment>.
655 =head1 METHODS AND FUNCTIONS
657 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
661 Returns the response error code.
665 Returns the response error description text.
667 =head2 server_response
669 Returns the complete response from the server.
671 =head1 Handling of content(%content) data:
675 The following actions are valid
681 =head1 Setting vSecureProcessing parameters from content(%content)
683 The following rules are applied to map data to vSecureProcessing parameters
684 from content(%content):
686 # param => $content{<key>}
687 AccountNumber => 'card_number',
689 ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration'
690 ExpirationYear => \( $year ), # YY from MM/YY of 'expiration'
693 CardHolderFirstName => 'first_name',
694 CardHolderLastName => 'last_name',
696 AvsStreet => 'address',
700 IndustryType => 'IndustryInfo',
706 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document Version: 140901 (September 1, 2014).
708 See http://www.vsecureprocessing.com/ for more information.
712 Original author: Alex Brelsfoard
714 Current maintainer: Alex Brelsfoard
718 Copyright (c) 2015 Freeside Internet Services, Inc.
722 This program is free software; you can redistribute it and/or modify it under
723 the same terms as Perl itself.
727 Need a complete, open-source back-office and customer self-service solution?
728 The Freeside software includes support for credit card and electronic check
729 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
730 trouble ticketing, and customer signup and self-service web interfaces.
732 http://freeside.biz/freeside/
736 perl(1). L<Business::OnlinePayment>.