1 package Business::OnlinePayment::vSecureProcessing;
10 use Business::OnlinePayment;
11 use Business::OnlinePayment::HTTPS;
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'
75 # inistialize standard B::OP attributes
77 $self->$_( '' ) for qw/authorization
85 # B::OP creates the following accessors:
86 # server, port, path, test_transaction, transaction_type,
87 # server_response, is_success, authorization,
88 # result_code, error_message,
91 env platform userid gid tid appid action
92 cvv_response avs_response risk_score
95 $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
97 $self->port($options{'port'});
99 $self->server($options{'url'});
101 $self->gid($options{'gid'});
103 $self->tid($options{'tid'});
105 $self->platform($options{'platform'});
107 $self->appid($options{'appid'});
109 $self->env($options{'env'}) if (defined($options{'env'})); # '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 warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
140 $self->content(%content);
143 sub process_content {
145 $self->clean_content();
146 my %content = $self->content();
147 $self->action(($action_mapping{lc $content{'action'}}) ? $action_mapping{lc $content{'action'}} : lc $content{'action'});
148 $self->path($payment_actions{ $self->action }{path});
149 $self->appid($content{appid}) if (!$self->appid && $content{appid});
155 # inistialize standard B::OP attributes
156 $self->is_success(0);
157 $self->$_( '' ) for qw/authorization
162 # clean and process the $self->content info
163 $self->process_content();
164 my %content = $self->content;
165 my $action = $self->action();
167 my @acceptable_actions = ('charge', 'refund', 'void');
169 unless ( grep { $action eq $_ } @acceptable_actions ) {
170 croak "'$action' is not supported at this time.";
173 # fill out the template vars
174 my $template_vars = {
177 platform => $self->platform,
178 userid => $self->userid,
184 amount => $content{'amount'},
185 track1 => ($content{'track1'}) ? $content{'track1'} : '',
186 track2 => ($content{'track2'}) ? $content{'track2'} : '',
187 type => ($content{'description'}) ? $content{'description'} : '',
188 cf1 => ($content{'UDField1'}) ? $content{'UDField1'} : '',
189 cf2 => ($content{'UDField2'}) ? $content{'UDField2'} : '',
191 account_number => ($content{'card_number'}) ? $content{'card_number'} : '',
192 exp_month => $content{'exp_month'},
193 exp_year => $content{'exp_year'},
194 cvv => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
195 first_name => ($content{'first_name'}) ? $content{'first_name'} : '',
196 last_name => ($content{'last_name'}) ? $content{'last_name'} : '',
197 postal_code => ($content{'zip'}) ? $content{'zip'} : '',
198 street_address => ($content{'address'}) ? $content{'address'} : '',
199 industry_type => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
200 invoice_num => ($content{'invoice_number'}) ? $content{'invoice_number'} : '',
201 appid => $self->appid(),
202 recurring => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
203 response_code => ($content{'response_code'}) ? $content{'response_code'} : '',
204 reference_number=> ($content{'ref_num'}) ? $content{'ref_num'} : '',
205 token => ($content{'token'}) ? $content{'token'} : '',
206 receipt => ($content{'receipt'}) ? $content{'receipt'} : '',
207 transaction_date=> ($content{'txn_date'}) ? $content{'txn_date'} : '',
208 merchant_data => ($content{'merchant_data'}) ? $content{'merchant_data'} : '',
211 # we won't be using level2 nor level3. So I'm leaving them blank for now.
216 ship_tp_postal_code => '',
217 ship_from_postal_code => '',
219 product_description1 => '',
220 product_description2 => '',
221 product_description3 => '',
222 product_description4 => ''
226 purchase_order_num => '',
229 alt_tax_amount => '',
230 discount_amount => '',
231 freight_amount => '',
233 line_item_count => '',
234 purchase_items => $self->_parse_line_items()
239 # create the list of required fields based on the action
240 my @required_fields = qw/ amount /;
241 if ($action eq 'charge') {
242 push(@required_fields, $_) foreach (qw/ account_number cvv exp_month exp_year /);
243 }elsif ($action eq 'void') {
244 push(@required_fields, $_) foreach (qw/ response_code reference_number receipt token transaction_date exp_month exp_year /);
245 }elsif ($action eq 'refund') {
246 push(@required_fields, $_) foreach (qw/ merchant_data token account_number exp_month exp_year /);
249 # check the requirements are met.
251 foreach my $field (@required_fields) {
252 push(@missing_fields, $field) if (!$template_vars->{payment}{$field});
254 if (scalar(@missing_fields)) {
255 croak "Missing required fields: ".join(', ', @missing_fields);
258 # read in the appropriate xml template
259 my $xml_template .= _get_xml_template( $action );
260 # create a template object.
261 my $tt = Template->new();
262 # populate the XML template.
264 $tt->process( \$xml_template, $template_vars, \$xml_data ) || croak $tt->error();
266 44rewdwarn "XML:\n$xml_data\n" if $DEBUG > 2;
268 my $opts = {'headers' => {}, 'Content-Type' => 'multipart/form-data'};
269 my $params = {param => $xml_data};
270 my ( $page, $server_response, %headers ) = $self->https_post( $opts, $params );
272 # store the server response.
273 $self->server_response($server_response);
274 # parse the result page.
275 $self->parse_response($page);
277 if (!$self->is_success() && !$self->error_message() ) {
279 #additional logging information, possibly too sensitive for an error msg
280 # (vSecureProcessing seems to have a failure mode where they return the full
281 # original request including card number)
282 $self->error_message(
283 "(HTTPS response: ".$server_response.") ".
285 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
286 "(Raw HTTPS content: ".$page.")"
289 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
295 # read $self->server_response and decipher any errors
300 if ($self->server_response =~ /^200/) {
301 my $response = XMLin($page);
302 $self->result_code($response->{Status});
303 $self->avs_response($response->{AvsResponse});
304 $self->cvv_response($response->{CvvResponse});
305 $self->is_success($self->result_code() eq '0' ? 1 : 0);
306 if ($self->is_success()) {
307 $self->authorization($response->{AuthIdentificationResponse});
309 # fill in error_message if there is is an error
310 if ( !$self->is_success && exists($response->{ResultCode})) {
311 $self->error_message('Error '.$response->{ResponseCode}.': '.$response->{ResultCode});
312 }elsif ( !$self->is_success && exists($response->{Receipt}) ) {
313 $self->error_message('Error '.$response->{ResponseCode}.': '.(exists($response->{Receipt})) ? $response->{Receipt} : '');
317 $self->is_success(0);
318 $self->error_message('Error communicating with vSecureProcessing server');
325 sub _get_xml_template {
328 my $xml_template = q|<Request >
330 <Platform>[% auth.platform %]</Platform>
331 <UserID>[% auth.userid %]</UserId>
332 <GID>[% auth.gid %]</GID>
333 <Tid>[% auth.tid %]</Tid>
337 if ($action eq 'charge') {
338 $xml_template .= _get_xml_template_charge();
339 }elsif($action eq 'void') {
340 $xml_template .= _get_xml_template_void();
341 }elsif($action eq 'authorize') {
342 $xml_template .= _get_xml_template_auth();
343 }elsif($action eq 'authorize_cancel') {
344 $xml_template .= _get_xml_template_auth_cancel();
345 }elsif($action eq 'refund') {
346 $xml_template .= _get_xml_template_refund();
347 }elsif($action eq 'capture') {
348 $xml_template .= _get_xml_template_capture();
349 }elsif($action eq 'create_token') {
350 $xml_template .= _get_xml_template_create_token();
351 }elsif($action eq 'delete_token') {
352 $xml_template .= _get_xml_template_delete_token();
353 }elsif($action eq 'query_token') {
354 $xml_template .= _get_xml_template_query_token();
355 }elsif($action eq 'update_exp_date') {
356 $xml_template .= _get_xml_template_update_exp_date();
357 }elsif($action eq 'update_token') {
358 $xml_template .= _get_xml_template_update_token();
361 $xml_template .= "\n</Request>";
363 return $xml_template;
366 sub _get_xml_template_charge {
367 my $xml_template = q|<ProcessPayment>
368 <Amount>[% payment.amount %]</Amount>
369 <Trk1>[% payment.track1 %]</Trk1>
370 <Trk2>[% payment.track2 %]</Trk2>
371 <TypeOfSale>[% payment.type %]</TypeOfSale>
372 <Cf1>[% payment.cf1 %]</Cf1>
373 <Cf2>[% payment.cf2 %]</Cf2>
374 <Cf3>[% payment.cf3 %]</Cf3>
375 <AccountNumber>[% payment.account_number %]</AccountNumber>
376 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
377 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
378 <Cvv>[% payment.cvv %]</Cvv>
379 <CardHolderFirstName>[% payment.first_name %]</CardHolderFirstName>
380 <CardHolderLastName>[% payment.last_name %]</CardHolderLastName>
381 <AvsZip>[% payment.postal_code %]</AvsZip>
382 <AvsStreet>[% payment.street_address %]</AvsStreet>
384 <IndType >[% payment.industry_type %]</IndType >
385 <IndInvoice>[% payment.invoice_num %]</IndInvoice>
387 <ApplicationId>[% payment.appid %]</ApplicationId>
388 <Recurring>[% payment.recurring %]</Recurring>
391 <Level2CardType>[% level2.card_type %]</Level2CardType >
392 <PurchaseCode>[% level2.purchase_code %]</PurchaseCode>
393 <ShipToCountryCode>[% level2.country_code %]</ShipToCountryCode>
394 <ShipToPostalCode>[% level2.ship_tp_postal_code %]</ShipToPostalCode>
395 <ShipFromPostalCode>[% level2.ship_from_postal_code %]</ShipFromPostalCode>
396 <SalesTax>[% level2.sales_tax %]</SalesTax>
397 <ProductDescription1>[% level2.product_description1 %]</ProductDescription1>
398 <ProductDescription2>[% level2.product_description2 %]</ProductDescription2>
399 <ProductDescription3>[% level2.product_description3 %]</ProductDescription3>
400 <ProductDescription4>[% level2.product_description4 %]</ProductDescription4>
401 </Level2PurchaseInfo>
403 <PurchaseOrderNumber>[% level3.purchase_order_num %]</PurchaseOrderNumber>
404 <OrderDate>[% level3.order_date %]</OrderDate>
405 <DutyAmount>[% level3.duty_amount %]</DutyAmount>
406 <AlternateTaxAmount>[% level3.alt_tax_amount %]</AlternateTaxAmount>
407 <DiscountAmount>[% level3.discount_amount %]</DiscountAmount>
408 <FreightAmount>[% level3.freight_amount %]</FreightAmount>
409 <TaxExemptFlag>[% level3.tax_exempt %]</TaxExemptFlag>
410 <LineItemCount>[% level3.line_item_count %]</LineItemCount>
412 [% level3.purchase_items %]
414 </Level3PurchaseInfo>|;
416 return $xml_template;
419 sub _parse_line_items {
421 my %content = $self->content();
423 return '' if (!$content{'items'});
426 my $template = q| <LineItem>
427 <ItemSequenceNumber>[% seq_num %]</ItemSequenceNumber>
428 <ItemCode>[% code %]</ItemCode>
429 <ItemDescription>[% desc %]</ItemDescription>
430 <ItemQuantity>[% qty %]</ItemQuantity>
431 <ItemUnitOfMeasure>[% unit %]</ItemUnitOfMeasure>
432 <ItemUnitCost>[% unit_cost %]</ItemUnitCost>
433 <ItemAmount>[% amount %]</ItemAmount>
434 <ItemDiscountAmount>[% discount_amount %]</ItemDiscountAmount>
435 <ItemTaxAmount>[% tax_amount %]</ItemTaxAmount>
436 <ItemTaxRate>[% tax_rate %]</ItemTaxRate>
440 my @items = $content{'items'};
441 foreach my $item (@items) {
442 # fille in the slots from $template with details in $item
443 # push to @line_items
446 return join("\n", @line_items);
449 sub _get_xml_template_void {
450 my $xml_template = q|<ProcessVoid>
451 <Amount>[% payment.amount %]</Amount>
452 <AccountNumber>[% payment.account_number %]</AccountNumber>
453 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
454 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
455 <ReferenceNumber>[% payment.ref_num %]</ReferenceNumber>
457 <IndustryType1>[% payment.industry_type %]</IndustryType1>
458 <ApplicationId>[% payment.appid %]</ApplicationId>
461 return $xml_template;
464 sub _get_xml_template_refund {
465 my $xml_template = q|<ProcessRefund>
466 <Amount>[% payment.amount %]</Amount>
467 <AccountNumber>[% payment.account_number %]</AccountNumber>
468 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
469 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
470 <ApplicationId>[% payment.appid %]</ApplicationId>
473 return $xml_template;
476 sub _get_xml_template_auth {
477 my $xml_template = '';
479 return $xml_template;
482 sub _get_xml_template_auth_cancel {
483 my $xml_template = '';
485 return $xml_template;
488 sub _get_xml_template_capture {
489 my $xml_template = '';
491 return $xml_template;
494 sub _get_xml_template_create_token {
495 my $xml_template = '';
497 return $xml_template;
500 sub _get_xml_template_delete_token {
501 my $xml_template = '';
503 return $xml_template;
506 sub _get_xml_template_query_token {
507 my $xml_template = '';
509 return $xml_template;
512 sub _get_xml_template_update_exp_date {
513 my $xml_template = '';
515 return $xml_template;
518 sub _get_xml_template_update_token {
519 my $xml_template = '';
521 return $xml_template;
531 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
535 use Business::OnlinePayment;
536 my %processor_info = (
538 gid => 12345678901234567890,
541 url => 'www.####.com'
544 new Business::OnlinePayment( "vSecureProcessing", %processor_info);
548 action => 'Normal Authorization',
549 description => 'Business::OnlinePayment test',
551 customer_id => 'tfb',
552 name => 'Tofu Beast',
553 address => '123 Anystreet',
557 card_number => '4007000000027',
558 expiration => '09/02',
559 cvv2 => '1234', #optional
563 if($tx->is_success()) {
564 print "Card processed successfully: ".$tx->authorization."\n";
566 print "Card was rejected: ".$tx->error_message."\n";
571 For detailed information see L<Business::OnlinePayment>.
573 =head1 METHODS AND FUNCTIONS
575 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
579 Returns the response error code.
583 Returns the response error description text.
585 =head2 server_response
587 Returns the complete response from the server.
589 =head1 Handling of content(%content) data:
593 The following actions are valid
599 =head1 Setting vSecureProcessing parameters from content(%content)
601 The following rules are applied to map data to vSecureProcessing parameters
602 from content(%content):
604 # param => $content{<key>}
605 AccountNumber => 'card_number',
607 ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration'
608 ExpirationYear => \( $year ), # YY from MM/YY of 'expiration'
611 CardHolderFirstName => 'first_name',
612 CardHolderLastName => 'last_name',
614 AvsStreet => 'address',
618 IndustryType => 'IndustryInfo',
624 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document Version: 140901 (September 1, 2014).
626 See http://www.vsecureprocessing.com/ for more information.
630 Original author: Alex Brelsfoard
632 Current maintainer: Alex Brelsfoard
636 Copyright (c) 2015 Freeside Internet Services, Inc.
640 This program is free software; you can redistribute it and/or modify it under
641 the same terms as Perl itself.
645 Need a complete, open-source back-office and customer self-service solution?
646 The Freeside software includes support for credit card and electronic check
647 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
648 trouble ticketing, and customer signup and self-service web interfaces.
650 http://freeside.biz/freeside/
654 perl(1). L<Business::OnlinePayment>.