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 cvv_response
91 avs_response risk_score txn_amount txn_date
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 warn "RESPONSE: \n".Dumper($response)."\n";
327 $self->result_code($response->{Status});
328 $self->avs_response($response->{AvsResponse});
329 $self->cvv_response($response->{CvvResponse});
330 $self->txn_date($response->{TransactionDate});
331 $self->txn_amount($response->{TransactionAmount} / 100);
332 $self->cvv_response($response->{CvvResponse});
333 $self->is_success($self->result_code() eq '0' ? 1 : 0);
334 if ($self->is_success()) {
335 $self->authorization($response->{AuthIdentificationResponse});
337 # fill in error_message if there is is an error
338 if ( !$self->is_success && exists($response->{ResultCode})) {
339 $self->error_message('Error '.$response->{ResponseCode}.': '.$response->{ResultCode});
340 }elsif ( !$self->is_success && exists($response->{Receipt}) ) {
341 $self->error_message('Error '.$response->{ResponseCode}.': '.(exists($response->{Receipt})) ? $response->{Receipt} : '');
345 $self->is_success(0);
346 $self->error_message('Error communicating with vSecureProcessing server');
353 sub _get_xml_template {
356 my $xml_template = q|<Request >
358 <Platform>[% auth.platform %]</Platform>
359 <UserId>[% auth.userid %]</UserId>
360 <GID>[% auth.gid %]</GID>
361 <Tid>[% auth.tid %]</Tid>
365 if ($action eq 'charge') {
366 $xml_template .= _get_xml_template_charge();
367 }elsif($action eq 'void') {
368 $xml_template .= _get_xml_template_void();
369 }elsif($action eq 'authorize') {
370 $xml_template .= _get_xml_template_auth();
371 }elsif($action eq 'authorize_cancel') {
372 $xml_template .= _get_xml_template_auth_cancel();
373 }elsif($action eq 'refund') {
374 $xml_template .= _get_xml_template_refund();
375 }elsif($action eq 'capture') {
376 $xml_template .= _get_xml_template_capture();
377 }elsif($action eq 'create_token') {
378 $xml_template .= _get_xml_template_create_token();
379 }elsif($action eq 'delete_token') {
380 $xml_template .= _get_xml_template_delete_token();
381 }elsif($action eq 'query_token') {
382 $xml_template .= _get_xml_template_query_token();
383 }elsif($action eq 'update_exp_date') {
384 $xml_template .= _get_xml_template_update_exp_date();
385 }elsif($action eq 'update_token') {
386 $xml_template .= _get_xml_template_update_token();
389 $xml_template .= "</Request>";
390 $xml_template =~ s/[\n\t\s]*//g;
392 return $xml_template;
395 sub _get_xml_template_charge {
396 my $xml_template = q|<ProcessPayment>
397 <Amount>[% payment.amount %]</Amount>
398 <Trk1>[% payment.track1 %]</Trk1>
399 <Trk2>[% payment.track2 %]</Trk2>
400 <TypeOfSale>[% payment.type %]</TypeOfSale>
401 <Cf1>[% payment.cf1 %]</Cf1>
402 <Cf2>[% payment.cf2 %]</Cf2>
403 <Cf3>[% payment.cf3 %]</Cf3>
404 <AccountNumber>[% payment.account_number %]</AccountNumber>
405 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
406 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
407 <Cvv>[% payment.cvv %]</Cvv>
408 <CardHolderFirstName>[% payment.first_name %]</CardHolderFirstName>
409 <CardHolderLastName>[% payment.last_name %]</CardHolderLastName>
410 <AvsZip>[% payment.postal_code %]</AvsZip>
411 <AvsStreet>[% payment.street_address %]</AvsStreet>
413 <IndType >[% payment.industry_type %]</IndType >
414 <IndInvoice>[% payment.invoice_num %]</IndInvoice>
416 <ApplicationId>[% payment.appid %]</ApplicationId>
417 <Recurring>[% payment.recurring %]</Recurring>
420 # other options (that we are not using right now):
421 # <Level2PurchaseInfo>
422 # <Level2CardType>[% level2.card_type %]</Level2CardType >
423 # <PurchaseCode>[% level2.purchase_code %]</PurchaseCode>
424 # <ShipToCountryCode>[% level2.country_code %]</ShipToCountryCode>
425 # <ShipToPostalCode>[% level2.ship_tp_postal_code %]</ShipToPostalCode>
426 # <ShipFromPostalCode>[% level2.ship_from_postal_code %]</ShipFromPostalCode>
427 # <SalesTax>[% level2.sales_tax %]</SalesTax>
428 # <ProductDescription1>[% level2.product_description1 %]</ProductDescription1>
429 # <ProductDescription2>[% level2.product_description2 %]</ProductDescription2>
430 # <ProductDescription3>[% level2.product_description3 %]</ProductDescription3>
431 # <ProductDescription4>[% level2.product_description4 %]</ProductDescription4>
432 # </Level2PurchaseInfo>
433 # <Level3PurchaseInfo>
434 # <PurchaseOrderNumber>[% level3.purchase_order_num %]</PurchaseOrderNumber>
435 # <OrderDate>[% level3.order_date %]</OrderDate>
436 # <DutyAmount>[% level3.duty_amount %]</DutyAmount>
437 # <AlternateTaxAmount>[% level3.alt_tax_amount %]</AlternateTaxAmount>
438 # <DiscountAmount>[% level3.discount_amount %]</DiscountAmount>
439 # <FreightAmount>[% level3.freight_amount %]</FreightAmount>
440 # <TaxExemptFlag>[% level3.tax_exempt %]</TaxExemptFlag>
441 # <LineItemCount>[% level3.line_item_count %]</LineItemCount>
443 # [% level3.purchase_items %]
445 # </Level3PurchaseInfo>
447 return $xml_template;
450 sub _parse_line_items {
452 my %content = $self->content();
454 return '' if (!$content{'items'});
457 my $template = q| <LineItem>
458 <ItemSequenceNumber>[% seq_num %]</ItemSequenceNumber>
459 <ItemCode>[% code %]</ItemCode>
460 <ItemDescription>[% desc %]</ItemDescription>
461 <ItemQuantity>[% qty %]</ItemQuantity>
462 <ItemUnitOfMeasure>[% unit %]</ItemUnitOfMeasure>
463 <ItemUnitCost>[% unit_cost %]</ItemUnitCost>
464 <ItemAmount>[% amount %]</ItemAmount>
465 <ItemDiscountAmount>[% discount_amount %]</ItemDiscountAmount>
466 <ItemTaxAmount>[% tax_amount %]</ItemTaxAmount>
467 <ItemTaxRate>[% tax_rate %]</ItemTaxRate>
471 my @items = $content{'items'};
472 foreach my $item (@items) {
473 # fille in the slots from $template with details in $item
474 # push to @line_items
477 return join("\n", @line_items);
480 sub _get_xml_template_void {
481 my $xml_template = q|<ProcessVoid>
482 <Amount>[% payment.amount %]</Amount>
483 <AccountNumber>[% payment.account_number %]</AccountNumber>
484 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
485 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
486 <ReferenceNumber>[% payment.reference_number %]</ReferenceNumber>
488 <IndustryType1>[% payment.industry_type %]</IndustryType1>
489 <ApplicationId>[% payment.appid %]</ApplicationId>
492 return $xml_template;
495 sub _get_xml_template_refund {
496 my $xml_template = q|<ProcessRefund>
497 <Amount>[% payment.amount %]</Amount>
498 <AccountNumber>[% payment.account_number %]</AccountNumber>
499 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
500 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
501 <ApplicationId>[% payment.appid %]</ApplicationId>
504 return $xml_template;
507 sub _get_xml_template_auth {
508 my $xml_template = '';
510 return $xml_template;
513 sub _get_xml_template_auth_cancel {
514 my $xml_template = '';
516 return $xml_template;
519 sub _get_xml_template_capture {
520 my $xml_template = '';
522 return $xml_template;
525 sub _get_xml_template_create_token {
526 my $xml_template = '';
528 return $xml_template;
531 sub _get_xml_template_delete_token {
532 my $xml_template = '';
534 return $xml_template;
537 sub _get_xml_template_query_token {
538 my $xml_template = '';
540 return $xml_template;
543 sub _get_xml_template_update_exp_date {
544 my $xml_template = '';
546 return $xml_template;
549 sub _get_xml_template_update_token {
550 my $xml_template = '';
552 return $xml_template;
562 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
566 use Business::OnlinePayment;
567 my %processor_info = (
569 gid => 12345678901234567890,
572 url => 'www.####.com'
575 new Business::OnlinePayment( "vSecureProcessing", %processor_info);
579 action => 'Normal Authorization',
580 description => 'Business::OnlinePayment test',
582 customer_id => 'tfb',
583 name => 'Tofu Beast',
584 address => '123 Anystreet',
588 card_number => '4007000000027',
589 expiration => '09/02',
590 cvv2 => '1234', #optional
594 if($tx->is_success()) {
595 print "Card processed successfully: ".$tx->authorization."\n";
597 print "Card was rejected: ".$tx->error_message."\n";
602 For detailed information see L<Business::OnlinePayment>.
604 =head1 METHODS AND FUNCTIONS
606 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
610 Returns the response error code.
614 Returns the response error description text.
616 =head2 server_response
618 Returns the complete response from the server.
620 =head1 Handling of content(%content) data:
624 The following actions are valid
630 =head1 Setting vSecureProcessing parameters from content(%content)
632 The following rules are applied to map data to vSecureProcessing parameters
633 from content(%content):
635 # param => $content{<key>}
636 AccountNumber => 'card_number',
638 ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration'
639 ExpirationYear => \( $year ), # YY from MM/YY of 'expiration'
642 CardHolderFirstName => 'first_name',
643 CardHolderLastName => 'last_name',
645 AvsStreet => 'address',
649 IndustryType => 'IndustryInfo',
655 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document Version: 140901 (September 1, 2014).
657 See http://www.vsecureprocessing.com/ for more information.
661 Original author: Alex Brelsfoard
663 Current maintainer: Alex Brelsfoard
667 Copyright (c) 2015 Freeside Internet Services, Inc.
671 This program is free software; you can redistribute it and/or modify it under
672 the same terms as Perl itself.
676 Need a complete, open-source back-office and customer self-service solution?
677 The Freeside software includes support for credit card and electronic check
678 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
679 trouble ticketing, and customer signup and self-service web interfaces.
681 http://freeside.biz/freeside/
685 perl(1). L<Business::OnlinePayment>.