1 package Business::OnlinePayment::vSecureProcessing;
7 use Business::OnlinePayment;
9 use HTTP::Request::Common;
11 use Template; # construct XML requests
12 use XML::Simple; # parse XML responses
14 use vars qw($VERSION $DEBUG @ISA $myself $server_root $port);
16 @ISA = qw(Business::OnlinePayment);
19 $myself = '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'
77 # B::OP creates the following accessors:
78 # server, port, path, test_transaction, transaction_type,
79 # server_response, is_success, authorization,
80 # result_code, error_message,
83 env platform userid gid tid appid action
84 cvv_response avs_response risk_score
87 $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
91 $self->server($options{'url'});
93 $self->gid($options{'gid'});
95 $self->tid($options{'tid'});
97 $self->platform($options{'platform'});
99 $self->appid($options{'appid'});
101 $self->env($options{'env'}) if (defined($options{'env'})); # 'live'/'test'
108 my ($self,$content) = @_;
109 my %content = $self->content();
112 no warnings 'uninitialized';
114 # strip non-digits from card number
115 my $card_number = '';
116 if ( $content{card_number} ) {
117 $content{card_number} =~ s/\D//g;
120 # separate month and year values for expiry_date
121 if ( $content{expiration} ) {
122 ($content{exp_month}, $content{exp_year}) = split /\//, $content{expiration};
123 $content{exp_month} = sprintf "%02d", $content{exp_month};
124 $content{exp_year} = substr($content{exp_year},0,2) if ($content{exp_year} > 99);
127 if (!$content{'first_name'} || !$content{'last_name'} && $content{'name'}) {
128 ($content{'first_name'}, $content{'last_name'}) = split(' ', $content{'name'}, 2);
131 warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
132 $self->content(%content);
135 sub process_content {
137 $self->clean_content();
138 my %content = $self->content();
139 $self->action(($action_mapping{lc $content{'action'}}) ? $action_mapping{lc $content{'action'}} : lc $content{'action'});
140 $self->path($payment_actions{ $self->action }{path});
141 $self->appid($content{appid}) if (!$self->appid && $content{appid});
147 # inistialize standard B::OP attributes
148 $self->is_success(0);
149 $self->$_( '' ) for qw/authorization
157 # clean and process the $self->content info
158 $self->process_content();
159 my %content = $self->content;
160 my $action = $self->action();
162 my @acceptable_actions = ('charge', 'refund', 'void');
164 unless ( grep { $action eq $_ } @acceptable_actions ) {
165 croak "'$action' is not supported at this time.";
168 # fill out the template vars
169 my $template_vars = {
172 platform => $self->platform,
173 userid => $self->userid,
179 amount => $content{'amount'},
180 track1 => ($content{'track1'}) ? $content{'track1'} : '',
181 track2 => ($content{'track2'}) ? $content{'track2'} : '',
182 type => ($content{'description'}) ? $content{'description'} : '',
183 cf1 => ($content{'UDField1'}) ? $content{'UDField1'} : '',
184 cf2 => ($content{'UDField2'}) ? $content{'UDField2'} : '',
186 account_number => ($content{'card_number'}) ? $content{'card_number'} : '',
187 exp_month => $content{'exp_month'},
188 exp_year => $content{'exp_year'},
189 cvv => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
190 first_name => ($content{'first_name'}) ? $content{'first_name'} : '',
191 last_name => ($content{'last_name'}) ? $content{'last_name'} : '',
192 postal_code => ($content{'zip'}) ? $content{'zip'} : '',
193 street_address => ($content{'address'}) ? $content{'address'} : '',
194 industry_type => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
195 invoice_num => ($content{'invoice_number'}) ? $content{'invoice_number'} : '',
196 appid => $self->appid(),
197 recurring => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
198 response_code => ($content{'response_code'}) ? $content{'response_code'} : '',
199 reference_number=> ($content{'ref_num'}) ? $content{'ref_num'} : '',
200 token => ($content{'token'}) ? $content{'token'} : '',
201 receipt => ($content{'receipt'}) ? $content{'receipt'} : '',
202 transaction_date=> ($content{'txn_date'}) ? $content{'txn_date'} : '',
203 merchant_data => ($content{'merchant_data'}) ? $content{'merchant_data'} : '',
206 # we won't be using level2 nor level3. So I'm leaving them blank for now.
211 ship_tp_postal_code => '',
212 ship_from_postal_code => '',
214 product_description1 => '',
215 product_description2 => '',
216 product_description3 => '',
217 product_description4 => ''
221 purchase_order_num => '',
224 alt_tax_amount => '',
225 discount_amount => '',
226 freight_amount => '',
228 line_item_count => '',
229 purchase_items => $self->_parse_line_items()
234 # create the list of required fields based on the action
235 my @required_fields = qw/ amount /;
236 if ($action eq 'charge') {
237 push(@required_fields, $_) foreach (qw/ account_number cvv exp_month exp_year /);
238 }elsif ($action eq 'void') {
239 push(@required_fields, $_) foreach (qw/ response_code reference_number receipt token transaction_date exp_month exp_year /);
240 }elsif ($action eq 'refund') {
241 push(@required_fields, $_) foreach (qw/ merchant_data token account_number exp_month exp_year /);
244 # check the requirements are met.
246 foreach my $field (@required_fields) {
247 push(@missing_fields, $field) if (!$template_vars->{payment}{$field});
249 if (scalar(@missing_fields)) {
250 croak "Missing required fields: ".join(', ', @missing_fields);
253 # read in the appropriate xml template
254 my $xml_template = _get_xml_template( $action );
255 # create a template object.
256 my $tt = Template->new();
257 # populate the XML template.
259 $tt->process( \$xml_template, $template_vars, \$xml_data ) || croak $tt->error();
261 warn "XML:\n$xml_data\n" if $DEBUG > 2;
264 my $ua = LWP::UserAgent->new;
265 my $page = $ua->post( $self->url . $self->path,
267 'param' => uri_escape($xml_data),
269 'content-type' => 'multipart/form-data'
272 warn "HTTPS Response: \n".Dumper($page)."\n" if $DEBUG > 1;
274 # store the server response.
275 $self->server_response($page->status_line);
276 # parse the result page.
277 $self->parse_response($page);
279 if (!$self->is_success() && !$self->error_message() ) {
281 #additional logging information, possibly too sensitive for an error msg
282 # (vSecureProcessing seems to have a failure mode where they return the full
283 # original request including card number)
284 $self->error_message(
285 "(HTTPS response: ".$page->status_line.") ".
286 "(Raw HTTPS content: ".$page->content.")"
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 ($page->is_success) {
301 my $response = XMLin($page->content);
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 {
330 if ($action eq 'charge') {
331 $xml_template = _get_xml_template_charge();
332 }elsif($action eq 'void') {
333 $xml_template = _get_xml_template_void();
334 }elsif($action eq 'authorize') {
335 $xml_template = _get_xml_template_auth();
336 }elsif($action eq 'authorize_cancel') {
337 $xml_template = _get_xml_template_auth_cancel();
338 }elsif($action eq 'refund') {
339 $xml_template = _get_xml_template_refund();
340 }elsif($action eq 'capture') {
341 $xml_template = _get_xml_template_capture();
342 }elsif($action eq 'create_token') {
343 $xml_template = _get_xml_template_create_token();
344 }elsif($action eq 'delete_token') {
345 $xml_template = _get_xml_template_delete_token();
346 }elsif($action eq 'query_token') {
347 $xml_template = _get_xml_template_query_token();
348 }elsif($action eq 'update_exp_date') {
349 $xml_template = _get_xml_template_update_exp_date();
350 }elsif($action eq 'update_token') {
351 $xml_template = _get_xml_template_update_token();
354 return $xml_template;
357 sub _get_xml_template_charge {
358 my $xml_template = q|<Request >
360 <Platform>[% auth.platform %]</Platform>
361 <UserID>[% auth.userid %]</UserId>
362 <GID>[% auth.gid %]</GID>
363 <Tid>[% auth.tid %]</Tid>
366 <Amount>[% payment.amount %]</Amount>
367 <Trk1>[% payment.track1 %]</Trk1>
368 <Trk2>[% payment.track2 %]</Trk2>
369 <TypeOfSale>[% payment.type %]</TypeOfSale>
370 <Cf1>[% payment.cf1 %]</Cf1>
371 <Cf2>[% payment.cf2 %]</Cf2>
372 <Cf3>[% payment.cf3 %]</Cf3>
373 <AccountNumber>[% payment.account_number %]</AccountNumber>
374 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
375 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
376 <Cvv>[% payment.cvv %]</Cvv>
377 <CardHolderFirstName>[% payment.first_name %]</CardHolderFirstName>
378 <CardHolderLastName>[% payment.last_name %]</CardHolderLastName>
379 <AvsZip>[% payment.postal_code %]</AvsZip>
380 <AvsStreet>[% payment.street_address %]</AvsStreet>
382 <IndType >[% payment.industry_type %]</IndType >
383 <IndInvoice>[% payment.invoice_num %]</IndInvoice>
385 <ApplicationId>[% payment.appid %]</ApplicationId>
386 <Recurring>[% payment.recurring %]</Recurring>
389 <Level2CardType>[% level2.card_type %]</Level2CardType >
390 <PurchaseCode>[% level2.purchase_code %]</PurchaseCode>
391 <ShipToCountryCode>[% level2.country_code %]</ShipToCountryCode>
392 <ShipToPostalCode>[% level2.ship_tp_postal_code %]</ShipToPostalCode>
393 <ShipFromPostalCode>[% level2.ship_from_postal_code %]</ShipFromPostalCode>
394 <SalesTax>[% level2.sales_tax %]</SalesTax>
395 <ProductDescription1>[% level2.product_description1 %]</ProductDescription1>
396 <ProductDescription2>[% level2.product_description2 %]</ProductDescription2>
397 <ProductDescription3>[% level2.product_description3 %]</ProductDescription3>
398 <ProductDescription4>[% level2.product_description4 %]</ProductDescription4>
399 </Level2PurchaseInfo>
401 <PurchaseOrderNumber>[% level3.purchase_order_num %]</PurchaseOrderNumber>
402 <OrderDate>[% level3.order_date %]</OrderDate>
403 <DutyAmount>[% level3.duty_amount %]</DutyAmount>
404 <AlternateTaxAmount>[% level3.alt_tax_amount %]</AlternateTaxAmount>
405 <DiscountAmount>[% level3.discount_amount %]</DiscountAmount>
406 <FreightAmount>[% level3.freight_amount %]</FreightAmount>
407 <TaxExemptFlag>[% level3.tax_exempt %]</TaxExemptFlag>
408 <LineItemCount>[% level3.line_item_count %]</LineItemCount>
410 [% level3.purchase_items %]
412 </Level3PurchaseInfo>
415 return $xml_template;
418 sub _parse_line_items {
420 my %content = $self->content();
422 return '' if (!$content{'items'});
425 my $template = q| <LineItem>
426 <ItemSequenceNumber>[% seq_num %]</ItemSequenceNumber>
427 <ItemCode>[% code %]</ItemCode>
428 <ItemDescription>[% desc %]</ItemDescription>
429 <ItemQuantity>[% qty %]</ItemQuantity>
430 <ItemUnitOfMeasure>[% unit %]</ItemUnitOfMeasure>
431 <ItemUnitCost>[% unit_cost %]</ItemUnitCost>
432 <ItemAmount>[% amount %]</ItemAmount>
433 <ItemDiscountAmount>[% discount_amount %]</ItemDiscountAmount>
434 <ItemTaxAmount>[% tax_amount %]</ItemTaxAmount>
435 <ItemTaxRate>[% tax_rate %]</ItemTaxRate>
439 my @items = $content{'items'};
440 foreach my $item (@items) {
441 # fille in the slots from $template with details in $item
442 # push to @line_items
445 return join("\n", @line_items);
448 sub _get_xml_template_void {
449 my $xml_template = q|<Request >
451 <Platform>[% auth.platform %]</Platform>
452 <UserID>[% auth.userid %]</UserId>
453 <GID>[% auth.gid %]</GID>
454 <Tid>[% auth.tid %]</Tid>
458 <AccountNumber>[% payment.account_number %]</AccountNumber>
459 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
460 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
461 <ReferenceNumber>[% payment.ref_num %]</ReferenceNumber>
463 <IndustryType1>[% payment.industry_type %]</IndustryType1>
464 <ApplicationId>[% payment.appid %]</ApplicationId>
468 return $xml_template;
471 sub _get_xml_template_refund {
472 my $xml_template = q|<Request>
474 <Platform>[% auth.platform %]</Platform>
475 <UserID>[% auth.userid %]</UserId>
476 <GID>[% auth.gid %]</GID>
477 <Tid>[% auth.tid %]</Tid>
480 <Amount>[% payment.amount %]</Amount>
481 <AccountNumber>[% payment.account_number %]</AccountNumber>
482 <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
483 <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
484 <ApplicationId>[% payment.appid %]</ApplicationId>
488 return $xml_template;
491 sub _get_xml_template_auth {
492 my $xml_template = '';
494 return $xml_template;
497 sub _get_xml_template_auth_cancel {
498 my $xml_template = '';
500 return $xml_template;
503 sub _get_xml_template_capture {
504 my $xml_template = '';
506 return $xml_template;
509 sub _get_xml_template_create_token {
510 my $xml_template = '';
512 return $xml_template;
515 sub _get_xml_template_delete_token {
516 my $xml_template = '';
518 return $xml_template;
521 sub _get_xml_template_query_token {
522 my $xml_template = '';
524 return $xml_template;
527 sub _get_xml_template_update_exp_date {
528 my $xml_template = '';
530 return $xml_template;
533 sub _get_xml_template_update_token {
534 my $xml_template = '';
536 return $xml_template;
546 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
550 use Business::OnlinePayment;
551 my %processor_info = (
553 gid => 12345678901234567890,
556 url => 'www.####.com'
559 new Business::OnlinePayment( "vSecureProcessing", %processor_info);
563 action => 'Normal Authorization',
564 description => 'Business::OnlinePayment test',
566 customer_id => 'tfb',
567 name => 'Tofu Beast',
568 address => '123 Anystreet',
572 card_number => '4007000000027',
573 expiration => '09/02',
574 cvv2 => '1234', #optional
578 if($tx->is_success()) {
579 print "Card processed successfully: ".$tx->authorization."\n";
581 print "Card was rejected: ".$tx->error_message."\n";
586 For detailed information see L<Business::OnlinePayment>.
588 =head1 METHODS AND FUNCTIONS
590 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
594 Returns the response error code.
598 Returns the response error description text.
600 =head2 server_response
602 Returns the complete response from the server.
604 =head1 Handling of content(%content) data:
608 The following actions are valid
614 =head1 Setting vSecureProcessing parameters from content(%content)
616 The following rules are applied to map data to vSecureProcessing parameters
617 from content(%content):
619 # param => $content{<key>}
620 AccountNumber => 'card_number',
622 ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration'
623 ExpirationYear => \( $year ), # YY from MM/YY of 'expiration'
626 CardHolderFirstName => 'first_name',
627 CardHolderLastName => 'last_name',
629 AvsStreet => 'address',
633 IndustryType => 'IndustryInfo',
639 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document Version: 140901 (September 1, 2014).
641 See http://www.vsecureprocessing.com/ for more information.
645 Original author: Alex Brelsfoard
647 Current maintainer: Alex Brelsfoard
651 Need a complete, open-source back-office and customer self-service solution?
652 The Freeside software includes support for credit card and electronic check
653 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
654 trouble ticketing, and customer signup and self-service web interfaces.
656 http://freeside.biz/freeside/
660 perl(1). L<Business::OnlinePayment>.