diff --git a/.github/workflows/ruby-ci.yml b/.github/workflows/ruby-ci.yml index 654a004b643..1275083a680 100644 --- a/.github/workflows/ruby-ci.yml +++ b/.github/workflows/ruby-ci.yml @@ -17,8 +17,6 @@ jobs: strategy: matrix: version: - - 2.5 - - 2.6 - 2.7 gemfile: - gemfiles/Gemfile.rails50 @@ -42,3 +40,5 @@ jobs: - name: Test run: bundle exec rake test + - name: Linter + run: bundle exec rubocop diff --git a/.rubocop.yml b/.rubocop.yml index 50d63c3dd84..f012a5c1777 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -15,7 +15,7 @@ AllCops: - "lib/active_merchant/billing/gateways/paypal_express.rb" - "vendor/**/*" ExtraDetails: false - TargetRubyVersion: 2.5 + TargetRubyVersion: 2.7 # Active Merchant gateways are not amenable to length restrictions Metrics/ClassLength: @@ -33,7 +33,7 @@ Layout/DotPosition: Layout/CaseIndentation: EnforcedStyle: end -Layout/IndentHash: +Layout/IndentFirstHashElement: EnforcedStyle: consistent Naming/PredicateName: diff --git a/CHANGELOG b/CHANGELOG index fb3c0c7e48c..dc546afb4b6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,352 @@ = ActiveMerchant CHANGELOG == HEAD +* Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 +* TNS: Use the specified order_id in request if available [yunnydang] #4880 +* Cybersource: Support recurring apple pay [aenand] #4874 +* Verve BIN ranges and add card type to Rapyd gateway [jherreraa] #4875 +* Rapyd: Add network_reference_id, initiation_type, and update stored credential method [yunnydang] #4877 +* Adyen: Add the store field [yunnydang] #4878 +* Stripe Payment Intents: Expand balance txns for regular transactions [yunnydang] #4882 +* CyberSource (SOAP): Added support for 3DS exemption request fields [BritneyS] #4881 +* StripePI: Adding network tokenization fields to Stripe PaymentIntents [BritneyS] #4867 +* Shift4: Fixing currency bug [Heavyblade] #4887 +* Rapyd: fixing issue with json encoding and signatures [Heavyblade] #4892 +* SumUp: Setup, Scrub and Purchase build [sinourain] #4890 +* XpayGateway: Initial setup [javierpedrozaing] #4889 +* Rapyd: Add validation to not send cvv and network_reference_id [javierpedrozaing] #4895 +* Ebanx: Add Ecuador and Bolivia as supported countries [almalee24] #4893 +* Decidir: Add support for network tokens [almalee24] #4870 +* Element: Fix credit card name bug [almalee24] #4898 +* Adyen: Add payout endpoint [almalee24] #4885 +* Adding Oauth Response for access tokens [almalee24] #4851 +* CheckoutV2: Update stored credentials [almalee24] #4901 +* Revert "Adding Oauth Response for access tokens" [almalee24] #4906 +* Adyen: Fix shopperEmail bug [almalee24] #4904 +* Add Cabal card bin ranges [yunnydang] #4908 +* Kushki: Fixing issue with 3DS info on visa cc [heavyblade] #4899 +* Adyen: Add MIT flagging for Network Tokens [aenand] #4905 +* Moneris: Update sca actions [almalee24] #4902 +* Ogone: Add gateway specific 3ds option with default options mapping [jherreraa] #4894 +* Rapyd: Add recurrence_type field [yunnydang] #4912 +* Revert "Adyen: Update MIT flagging for NT" [almalee24] #4914 +* SumUp: Void and partial refund calls [sinourain] #4891 + +== Version 1.135.0 (August 24, 2023) +* PaymentExpress: Correct endpoints [steveh] #4827 +* Adyen: Add option to elect which error message [aenand] #4843 +* Reach: Update list of supported countries [jcreiff] #4842 +* Paysafe: Truncate address fields [jcreiff] #4841 +* Braintree: Support third party Network Tokens [aenand] #4775 +* Kushki: Fix add amount default method for subtotalIva and subtotalIva0 [yunnydang] #4845 +* Rapyd: Add customer object to requests [aenand] #4838 +* CyberSource: Add merchant_id [almalee24] #4844 +* Global Collect: Add agent numeric code and house number field [yunnydang] #4847 +* Deepstack: Add Deepstack Gateway [khoinguyendeepstack] #4830 +* Braintree: Additional tests for credit transactions [jcreiff] #4848 +* Rapyd: Change nesting of description, statement_descriptor, complete_payment_url, and error_payment_url [jcreiff] #4849 +* Rapyd: Add merchant_reference_id [jcreiff] #4858 +* Braintree: Return error for ACH on credit [jcreiff] #4859 +* Rapyd: Update handling of ewallet and billing address phone [jcreiff] #4863 +* IPG: Change credentials inputs to use a combined store and user ID string as the user ID input [kylene-spreedly] #4854 +* Braintree Blue: Update the credit card details transaction hash [yunnydang] #4865 +* VisaNet Peru: Update generate_purchase_number_stamp [almalee24] #4855 +* Braintree: Add sca_exemption [almalee24] #4864 +* Ebanx: Update Verify [almalee24] #4866 + +== Version 1.134.0 (July 25, 2023) +* Update required Ruby version [almalee24] #4823 +* Kushki: Enable 3ds2 [jherreraa] #4832 + +== Version 1.133.0 (July 20, 2023) +* CyberSource: remove credentials from tests [bbraschi] #4836 +* Paysafe: Map order_id to merchantRefNum [jcreiff] #4839 +* Stripe PI: Gate sending NTID [almalee24] #4828 + +== Version 1.132.0 (July 20, 2023) +* Stripe Payment Intents: Add support for new card on file field [aenand] #4807 +* Commerce Hub: Add `physicalGoodsIndicator` and `schemeReferenceTransactionId` GSFs [sinourain] #4786 +* Nuvei (formerly SafeCharge): Add customer details to credit action [yunnydang] #4820 +* IPG: Update live url to correct endpoint [curiousepic] #4121 +* VPos: Adding Panal Credit Card type [jherreraa] #4814 +* Stripe PI: Update parameters for creation of customer [almalee24] #4782 +* WorldPay: Update xml tag for Credit Cards [almalee24] #4797 +* PaywayDotCom: update `live_url` [jcreiff] #4824 +* Stripe & Stripe PI: Update login key validation [almalee24] #4816 +* CheckoutV2: Parse the AVS and CVV checks more often [aenand] #4822 +* NMI: Add shipping_firstname, shipping_lastname, shipping_email, and surcharge fields [jcreiff] #4825 +* Borgun: Update authorization_from & message_from [almalee24] #4826 +* Kushki: Add Brazil as supported country [almalee24] #4829 +* Adyen: Add additional data for airline and lodging [javierpedrozaing] #4815 +* MIT: Changed how the payload was sent to the gateway [alejandrofloresm] #4655 +* SafeCharge: Add unreferenced_refund field [yunnydang] #4831 +* CyberSource: include `paymentSolution` for ApplePay and GooglePay [bbraschi] #4835 + +== Version 1.131.0 (June 21, 2023) +* Redsys: Add supported countries [jcreiff] #4811 +* Authorize.net: Truncate nameOnAccount for bank refunds [jcreiff] #4808 +* CheckoutV2: Add support for several customer data fields [rachelkirk] #4800 +* Worldpay: check payment_method responds to payment_cryptogram and eci [bbraschi] #4812 + +== Version 1.130.0 (June 13th, 2023) +* Payu Latam - Update error code method to surface network code [yunnydang] #4773 +* CyberSource: Handling Canadian bank accounts [heavyblade] #4764 +* CyberSource Rest: Fixing currency detection [heavyblade] #4777 +* CyberSource: Allow business rules for requests with network tokens [aenand] #4764 +* Adyen: Update Mastercard error messaging [kylene-spreedly] #4770 +* Authorize.net: Update mapping for billing address phone number [jcreiff] #4778 +* Braintree: Update mapping for billing address phone number [jcreiff] #4779 +* CommerceHub: Enabling multi-use public key encryption [jherreraa] #4771 +* Ogone: Enable 3ds Global for Ogone Gateway [javierpedrozaing] #4776 +* Worldpay: Fix Google Pay [almalee24] #4774 +* Borgun change default TrCurrencyExponent and MerchantReturnUrl [naashton] #4788 +* Borgun: support for GBP currency [naashton] #4789 +* CyberSource: Enable auto void on r230 [aenand] #4794 +* Redsys: Set appropriate request fields for stored credentials with CITs and MITs [BritneyS] #4784 +* Stripe & Stripe PI: Validate API Key [almalee24] #4801 +* Add BIN for Maestro [jcreiff] #4799 +* D_Local: Add save field on card object [yunnydang] #4805 +* PayPal Express: Adds support for MsgSubID property on DoReferenceTransaction and DoExpressCheckoutPayment [wikiti] #4798 +* Checkout_v2: use credit_card?, not case equality with CreditCard [bbraschi] #4803 +* Shift4: Enable general credit feature [jherreraa] #4790 + +== Version 1.129.0 (May 3rd, 2023) +* Adyen: Update selectedBrand mapping for Google Pay [jcreiff] #4763 +* Shift4: Add vendorReference field [jcreiff] #4762 +* Shift4: Add OAuth error [aenand] #4760 +* Stripe PI: Add billing address details to Apple Pay and Google Pay tokenization request [BritneyS] #4761 +* Make gem compatible with Ruby 3+ [pi3r] #4768 + +== Version 1.128.0 (April 24th, 2023) +* CheckoutV2: Add support for Shipping Address [nicolas-maalouf-cko] #4755 +* Element: Include Apple Pay - Google pay methods [jherrera] #4647 +* dLocal: Add transaction query API(s) request [almalee24] #4584 +* MercadoPago: Add transaction inquire request [molbrown] #4588 +* Worldpay: Add transaction inquire request [molbrown] #4592 +* Alelo: Adding homologation changes [heavyblade] #4590 +* Adyen: Map standard error codes for `processing_error`, `config_error`, `invalid_amount`, and `incorrect_address` [ajawadmirza] #4593 +* MerchantE: Add support for recurring transactions [naashton] #4594 +* CyberSource: Add support for `discount_management_indicator`, `purchase_tax_amount`, `installment_total_amount`, and `installment_annual_interest_rate` fields. [rachelkirk] #4595 +* Shift4: Remove `customer` from refund and `clerk` from store requests [ajawadmirza] #4596 +* TransFirst Transaction Express: Update xml prefixing to be compatible with Nokogiri 1.13.4 [dsmcclain] #4582 +* Iveri: Adding support for external MPI 3DS2 [heavyblade] #4598 +* Credorax: Pass Network Transaction ID on MIT [jherreraa] #4600 +* iVeri: Remove schema validation on Live requests [curiousepic] #4606 +* Beanstream: Adding Third Party 3DS fields [heavyblade] #4602 +* Borgun: Add support for 3DS preauth [ajawadmirza] #4603 +* Cardstream: Add third party 3ds2 support [sainterman] #4570 +* Accept both formats of Canadian routing numbers [molbrown] #4568 +* DLocal: Add support for `original_order_id` [rachelkirk] #4605 +* CheckoutV2: Add support for `merchant_initiated_transaction_id` [rachelkirk] #4611 +* CardConnect: Add stored credential & pass any valid `ecomind` field [ajawadmirza] #4609 +* Alelo: Trigger access token refresh on 404 [curiousepic] #4614 +* DLocal: Add Network Tokens [gasb150] #4608 +* Redsys: enable NTID generation with zero-value verify [jcreiff] #4615 +* IPG: Add support for passing in `store_id` on transactions [aenand] #4619 +* Adyen: Field support for Level 2 and level 3 information [sainterman] #4617 +* Add alternate alpha2 country code for Kosovo [jcreiff] #4622 +* CyberSource: Add support for several fields [rachelkirk] #4623 +* Reach: adding gateway [cristian] #4618 +* Orbital: integration improvements [molbrown] #4626 +* iVeri: add new url [almalee24] #4630 +* Payeezy: Enable Apple Pay support [naashton] #4631 +* Payeezy: Scrub Cryptogram [naashton] #4633 +* Checkout: Fix for `[:source][:stored]` in stored credentials [marioarranzr] #4629 +* Mundipagg: send authorization_secret_key on all transaction types [edgarv09] #4635 +* CommerceHub: Add new gateway [naashton] #4640 +* CyberSource: Update installment data method [rachelkirk] #4642 +* Element: fix bug with billing address email [jcreiff] #4644 +* Openpay: set URL by merchant country [edgarv09] #4637 +* Alelo: Improving credentials refresh process [heavyblade] #4616 +* Decidir: Add transaction inquire request [almalee24] #4649 +* EBANX: add soft_descriptor field [jcreiff] #4658 +* CommerceHub: Add Apple Pay and Google Pay [gasb150] #4648 +* Global Collect & Alelo: Fixing year dependent failing tests [heavyblade] #4665 +* Moneris: Add Google Pay [sinourain] #4666 +* Global Collect: Add transaction inquire request [almalee24] #4669 +* Stripe PI: Add Level 3 support [almalee24] #4673 +* Braintree: return additional processor response [jcreiff] #4653 +* Payeezy: name from `billing_address` on `purchase` [naashton] #4674 +* Stripe: add reverse_transfer to void transactions [jcreiff] #4668 +* Global Collect: fix bug on transaction inquire request [almalee24] #4676 +* Credorax: Support google pay and apple pay [edgarv09] #4661 +* Plexo: Add support for 5 new credit card brands (passcard, edenred, anda, tarjeta-d, sodexo) [edgarv09] #4652 +* Authorize.net: Google pay token support [sainterman] #4659 +* Credorax: Add support for Network Tokens [jherreraa] #4679 +* Stripe PI: use MultiResponse in create_setup_intent [jcreiff] #4683 +* Credorax: Correct NTID logic for MIT transactions [aenand] #4686 +* Adyen: Add support for `skip_mpi_data` flag [rachelkirk] #4654 +* Add Canadian Institution Numbers [jcreiff] #4687 +* Tns: update test URL [almalee24] #4698 +* TrustCommerce: Update `authorization_from` to handle `store` response [jherreraa] #4691 +* TrustCommerce: Verify feature added [jherreraa] #4692 +* Rapyd: Add customer object to transactions [javierpedrozaing] #4664 +* CybersourceRest: Add new gateway with authorize and purchase [heavyblade] #4690 +* Litle: Add prelive_url option [aenand] #4710 +* CommerceHub: Fixing verify status and prevent tokenization [heavyblade] #4716 +* Payeezy: Update Stored Credentials [almalee24] #4711 +* CheckoutV2: Add store/unstore [gasb150] #4712 +* CybersourceREST - Refund | Credit [sinourain] #4700 +* Braintree - Add Paypal custom fields [yunnydang] #4713 +* BlueSnap - Add descriptor phone number field [yunnydang] #4717 +* Braintree - Update transaction hash to include processor_authorization_code [yunnydang] #4718 +* CyberSourceRest: Add apple pay, google pay [gasb150] #4708 +* CybersourceREST - Void | Verify [sinourain] #4695 +* Credorax: Set default ECI values for token transactions [sainterman] #4693 +* CyberSourceRest: Add ACH Support [edgarv09] #4722 +* CybersourceREST: Add capture request [heavyblade] #4726 +* Paymentez: Add transaction inquire request [aenand] #4729 +* Ebanx: Add transaction inquire request [almalee24] #4725 +* Ebanx: Add support for Elo & Hipercard [almalee24] #4702 +* CheckoutV2: Add Idempotency key support [yunnydang] #4728 +* Adyen: Add support for shopper_statement field for capture [yunnydang] #4736 +* CheckoutV2: Update idempotency_key name [yunnydang] #4737 +* Payeezy: Enable external 3DS [jherreraa] #4715 +* Ebanx: Remove default email [aenand] #4747 +* CyberSourceRest: Add stored credentials support [jherreraa] #4707 +* Payeezy: Add `last_name` for `add_network_tokenization` [naashton] #4743 +* Stripe PI: Tokenize payment method at Stripe for `verify` [aenand] #4748 +* Kushki: Add support for the months and deferred fields [yunnydang] #4752 +* Borgun: Update TrCurrencyExponent for 3DS transactions with `ISK` [aenand] #4751 +* CyberSourceRest: Add gateway specific fields handling [jherreraa] #4746 +* IPG: Improve error handling [heavyblade] #4753 +* Shift4: Handle access token failed calls [heavyblade] #4745 +* Bogus: Add verify functionality [willemk] #4749 +* Litle: Update successful_from method [almalee24] #4765 + +== Version 1.127.0 (September 20th, 2022) +* BraintreeBlue: Add venmo profile_id [molbrown] #4512 +* Maestro: Adding missing BIN ranges [bradbroge] #4423 * Simetrik: Fix integer and float types, update scrub method [rachelkirk] #4405 +* Credorax: Convert country codes for `recipient_country_code` field [ajawadmirza] #4408 +* BlueSnap: Correctly parse `refund-transaction-id` [dsmcclain] #4411 +* Worldpay: Add level II and level III data [javierpedrozaing] #4393 +* Worldpay: extract `issuer_response_code` and `issuer_response_description` from gateway response [dsmcclain] #4412 +* Vantiv: Support `duplicate` field read from saleResponse.duplicate attr [mashton] #4413 +* Ogone: Add support for 3dsv2 [gasb150] #4410 +* BlueSnap: Add support for stored credentials [ajawadmirza] #4414 +* Monei: Add support for `lang` field [drkjc] #4421 +* Wompi: Redirect `refund` to `void` [drkjc] #4424 +* Rapyd: 3DS Support [naashton] #4422 +* Adyen: Update API version [jherreraa] #4418 +* Ogone: Updated home gateway URL [gasb150] #4419 +* Credorax: Update url gateway and credit cards [javierpedrozaing] #4417 +* Kushki: Pass extra_taxes with USD [therufs] #4426 +* DLocal: fix bug with `X-Idempotency-Key` header [dsmcclain] #4431 +* DLocal: Mark support for additional countries [gasb150] #4427 +* Rapyd: Additional Fields [naashton] #4434 +* Braintree: Return generated client token [BritneyS] #4416 +* Simetrik: Update `audience` field [simetrik-frank] #4433 +* CyberSource: Add bank account payment method support [heavyblade] #4428 +* Rapyd: Zero Dollar Auth [naashton] #4435 +* Rapyd: Scrub ACH [naashton] #4436 +* VisaNet Peru: Update `purchase_number` [rachelkirk] #4437 +* CardConnect: Add support for 3ds V2 [javierpedrozaing] #4429 +* Rapyd: Support `store` and `unstore` [naashton] #4439 +* Orbital: Update API version to 9.0 [gasb150] #4440 +* Plexo: Add `meta_data` fields and reorder amount object in response [ajawadmirza] #4441 +* Plexo: Change field name from `meta_data` to `metadata` [ajawadmirza] #4443 +* Simetrik: Update `vat` to be in cents [simetrik-frank] #4425 +* Cybersource: Handle Amex cryptograms [heavyblade] #4445 +* Rapyd: Pass fields to `refund` and `store` [naashton] #4449 +* VPOS: Allow reuse of encryption key [therufs] #4450 +* Orbital: Add `payment_action_ind` field and refund through credit card to support tandem implementation [ajawadmirza] #4420 +* Airwallex: Send `referrer_data` on setup transactions [drkjc] #4453 +* Adyen and StripPI: Updated error messaging [mbreenlyles] #4454 +* Airwallex: Update `referrer_data` field [drkjc] #4455 +* Simetrik: Update `order_id` and `description` to be top level fields [simetrik-frank] #4451 +* Plexo: Update `ip`, `description`, and `email` fields request format and scrub method to not filter cardholder name and reference id [ajawadmirza] #4457 +* Plexo: Update `verify` implementation and add `verify_amount` field [ajawadmirza] #4462 +* Vanco: Update `purchase` to complete a purchase transaction with an existing session id [BritneyS] #4461 +* Authorize.net: Allow custom verify_amount and validate it [jherreraa] #4464 +* Shift4: Add gateway adapter [ali-hassan] #4415 +* Rapyd: Correctly add `billing_address` [naashton] #4465 +* Credorax: Update processor response messages [jcreiff] #4466 +* Shift4: add `customer_reference`, `destination_postal_code`, `product_descriptors` fields and core refactoring [ajawadmirza] #4469 +* Paypal Express: Add checkout status to response object [mbreenlyles] #4467 +* Shift4: Scrub security code [naashton] #4470 +* Shift4: Update `cardOnFile` transaction requests [ajawadmirza] #4471 +* Plexo: Update `success_from` definition [ajawadmirza] #4468 +* Rapyd: Un-nest the payment urls [naashton] #4472 +* Paypal Express: Correct naming mistake for accessor [mbreenlyles] #4473 +* GlobalCollect: Enable Google Pay and Apple Pay [gasb150] #4388 +* Shift4: $0 auth [naashton] #4474 +* CyberSource: Updatie API version to 1.198 and fix 3DS test [cristian] #4456 +* Shift4: add `store` method, `present` field in card, and to pass amount in cents [ajawadmirza] #4475 +* Shift4: add `3ds2` implementation [ajawadmirza] #4476 +* Shift4: update `success_from` definition to consider response code [ajawadmirza] #4477 +* Rapyd: Customer Object [naashton] #4478 +* Shift4: Verify Endopint Fix [naashton] #4479 +* CheckoutV2: Scrub cryptogram and credit card number [ajawadmirza] #4488 +* CheckoutV2: Add `3ds.status` field to send status of 3DS flow of all 3DS transactions [BritneyS] #4492 +* CheckoutV2: Add `challenge_indicator`, `exemption`, `authorization_type`, `processing_channel_id`, and `capture_type` fields [ajawadmirza] #4482 +* Add `mada` card type and associated BINs; add support for `mada` in CheckoutV2 gateway [dsmcclain] #4486 +* Authorize.net: Refactor custom verify amount handling [jherreraa] #4485 +* EBANX: Change amount for Colombia [flaaviaa] #4481 +* Worldpay: Update `required_status_message` and `message_from` methods for response. [rachelkirk] #4493 +* CheckoutV2: Add support for transactions through OAuth [ajawadmirza] #4483 +* Vanco: Update unit test to remove remote call to gateway [ajawadmirza] #4497 +* Shift4: remove support for 3ds2 [ajawadmirza] #4503 +* Rapyd: Add support for stored credential [ajawadmirza] #4487 +* MerchantE: Update `store` and add `verify` method [ajawadmirza] #4507 +* Shift4: Add default `numericId`, add `InterfaceVersion`, `InterfaceName`, and `CompanyName` header fields, change date time format and allow merchant time zone [ajawadmirza] #4509 +* BraintreeBlue: Add support for partial capture [aenand] #4515 +* Rapyd: Change key name to `network_transaction_id` [ajawadmirza] #4514 +* CyberSource: Handle unsupported Network Token brands [heavyblade] #4500 +* Ingenico(Global Collect): Add support for `payment_product_id` [rachelkirk] #4521 +* Adyen: Add network transaction id to store call [jcreiff] #4522 +* Worldpay: Add machine cookie to subsequent calls during 3DS challenge [mbreenlyles] #4513* +* Shift4: Scrub `securityCode` fix [naashton] #4524 +* Credorax: Update `OpCode` for credit transactions [dsmcclain] #4279 +* CheckoutV2: Add `credit` method [ajawadmirza] #4490 +* Stripe Payment Intents: Add `options` for retrieve_setup_intent [aenand] #4529 +* CheckoutV2: Send payment id via `incremental_authorization` field [ajawadmirza] #4518 +* Shift4: Add card `present` field, use previous transaction authorization for capture, and hardcode header values [ajawadmirza] #4528 +* Orbital: Remove `DPANInd` field for RC transactions [ajawadmirza] #4502 +* EBANX: Add Spreedly tag to payment body [flaaviaa] #4527 +* Shift4: Add `expiration_date` field for refund transactions [ajawadmirza] #4532 +* Improve handling of AVS and CVV Results in Multiresponses [gasb150] #4516 +* Airwallex: Add `skip_3ds` field for create payment transactions [ajawadmirza] #4534 +* Shift4: Typo correction for `initial_transaction` [ajawadmirza] #4537 +* Rapyd: Pass Customer ID and fix `add_token` method [naashton] #4538 +* Shift4: If no timezone is sent on transactions, the code uses the hours and minutes as a timezone offset [ali-hassan] #4536 +* Priority: Add support for general credit and updating cvv and zip [priorityspreedly] #4517 +* Worldpay: Update actions for generated message in `required_status_message` method [rachelkirk] #4530 +* Adyen: Modify handling of countryCode for ACH [jcreiff] #4543 +* CardConnect: update api end-point urls [heavyblade] #4541 +* Vantiv(Litle): Add support for `fraudFilterOverride` field [rachelkirk] #4544 +* Stripe: Add shipping address [jcreiff] #4539 +* PayuLatam: Add extra1, extra2, extra3 fields [jcreiff] #4550 +* Paysafe: Add fundingTransaction object [jcreiff] #4552 +* MerchantE: Add tests for `moto_ecommerce_ind` field [ajawadmirza] #4554 +* Plexo: Update `purchase` method, add flags for header fields, add new fields `billing_address`, `identification_type`, `identification_value`, and `cardholder_birthdate` [ajawadmirza] #4540 +* Rapyd: Remove `BR`, `MX`, and `US` from supported countries [ajawadmirza] #4558 +* Stripe Payment Intents: fix bug with billing address email [jcreiff] #4556 +* Shift4: Add customer to `purchase` & `store` and remove transaction from `store` [ajawadmirza] #4557 +* MerchantE: only add `moto_commerce_ind` to request if it is present [ajawadmirza] #4560 +* Add BpPlus card type along with custom validation logic [dsmcclain] #4559 +* PayTrace: Support ACH implementation for new endpoints and request body [ajawadmirza] #4545 +* Rapyd: No force capture for ACH [naashton] #4562 +* Shift4: Applied checks on Shift4 Time/Timezone offset [ali-hassan] #4561 +* Alelo: Add gateway [heavyblade] #4555 +* Wompi: Allow partial refund amount on void_sync [jcreiff] #4535 +* Shift4: Timezone Offset [naashton] #4566 +* MerchantE: `recurring_pmt_num` and `recurring_pmt_count` fields [ali-hassan] #4553 +* Orbital: Add South African Rand to supported currencies [molbrown] #4569 +* Orbital: Fix CardSecValInd [molbrown] #4563 +* Shift4: Add `usage_indicator`, `indicator`, `scheduled_indicator`, and `transaction_id` fields [ajawadmirza] #4564 +* Shift4: Retrieve `access_token` once [naashton] #4572 +* Redsys: Update Base64 encryption handling for secret key [jcreiff] #4565 +* Shift4: refuse `postalCode` when its null [ajawadmirza] #4574 +* Plexo: Update param key to `refund_type` [ajawadmirza] #4575 +* Shift4: Update request params for `verify`, `capture`, and `refund` [ajawadmirza] #4577 +* CyberSource: Add support for `sec_code` [rachelkirk] #4581 +* BraintreeBlue: Correctly vault payment method token for PayPal Checkout with Vault [almalee24] #4579 +* BpPlus: Allow spaces in card number [ajawadmirza] #4585 +* Shift4: Decline referral transactions and parse message for internal server errors [ajawadmirza] #4583 +* Litle: Update homepage_url [gasb150] #4491 +* Priority: Update credential handling [therufs] #4571 +* Shift4: Fix authorization and remove `entryMode` from verify and store transactions [ajawadmirza] #4589 == Version 1.126.0 (April 15th, 2022) * Moneris: Add 3DS MPI field support [esmitperez] #4373 diff --git a/Gemfile b/Gemfile index cf6182f36dd..5f3d4397223 100644 --- a/Gemfile +++ b/Gemfile @@ -2,11 +2,13 @@ source 'https://rubygems.org' gemspec gem 'jruby-openssl', platforms: :jruby -gem 'rubocop', '~> 0.62.0', require: false +gem 'rubocop', '~> 0.72.0', require: false group :test, :remote_test do # gateway-specific dependencies, keeping these gems out of the gemspec - gem 'braintree', '>= 3.0.0', '<= 3.0.1' + gem 'braintree', '>= 4.14.0' + gem 'jose', '~> 1.1.3' gem 'jwe' gem 'mechanize' + gem 'timecop' end diff --git a/activemerchant.gemspec b/activemerchant.gemspec index e72702e8afc..a1e8ed4f5b6 100644 --- a/activemerchant.gemspec +++ b/activemerchant.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| s.email = 'tobi@leetsoft.com' s.homepage = 'http://activemerchant.org/' - s.required_ruby_version = '>= 2.5' + s.required_ruby_version = '>= 2.7' s.files = Dir['CHANGELOG', 'README.md', 'MIT-LICENSE', 'CONTRIBUTORS', 'lib/**/*', 'vendor/**/*'] s.require_path = 'lib' @@ -26,6 +26,7 @@ Gem::Specification.new do |s| s.add_dependency('builder', '>= 2.1.2', '< 4.0.0') s.add_dependency('i18n', '>= 0.6.9') s.add_dependency('nokogiri', '~> 1.4') + s.add_dependency('rexml', '~> 3.2.5') s.add_development_dependency('mocha', '~> 1') s.add_development_dependency('pry') diff --git a/circle.yml b/circle.yml index fcf9fe6fa42..949fa18bb15 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: ruby: - version: '2.5.0' + version: '2.7.0' dependencies: cache_directories: diff --git a/lib/active_merchant/billing/check.rb b/lib/active_merchant/billing/check.rb index 859cf600d5d..1d6feb931f2 100644 --- a/lib/active_merchant/billing/check.rb +++ b/lib/active_merchant/billing/check.rb @@ -7,8 +7,8 @@ module Billing #:nodoc: # You may use Check in place of CreditCard with any gateway that supports it. class Check < Model attr_accessor :first_name, :last_name, - :bank_name, :routing_number, :account_number, - :account_holder_type, :account_type, :number + :bank_name, :routing_number, :account_number, + :account_holder_type, :account_type, :number # Used for Canadian bank accounts attr_accessor :institution_number, :transit_number @@ -20,7 +20,7 @@ class Check < Model 309 310 315 320 338 340 509 540 608 614 623 809 815 819 828 829 837 839 865 879 889 899 241 242 248 250 265 275 277 290 294 301 303 307 311 314 321 323 327 328 330 332 334 335 342 343 346 352 355 361 362 366 370 372 - 376 378 807 853 890 + 376 378 807 853 890 618 842 ) def name @@ -60,20 +60,52 @@ def credit_card? false end + def valid_routing_number? + digits = routing_number.to_s.split('').map(&:to_i).select { |d| (0..9).cover?(d) } + case digits.size + when 9 + return checksum(digits) == 0 || CAN_INSTITUTION_NUMBERS.include?(routing_number[1..3]) + when 8 + return CAN_INSTITUTION_NUMBERS.include?(routing_number[5..7]) + end + + false + end + # Routing numbers may be validated by calculating a checksum and dividing it by 10. The # formula is: # (3(d1 + d4 + d7) + 7(d2 + d5 + d8) + 1(d3 + d6 + d9))mod 10 = 0 # See http://en.wikipedia.org/wiki/Routing_transit_number#Internal_checksums - def valid_routing_number? + def checksum(digits) + ((3 * (digits[0] + digits[3] + digits[6])) + + (7 * (digits[1] + digits[4] + digits[7])) + + (digits[2] + digits[5] + digits[8])) % 10 + end + + # Always return MICR-formatted routing number for Canadian routing numbers, US routing numbers unchanged + def micr_format_routing_number digits = routing_number.to_s.split('').map(&:to_i).select { |d| (0..9).cover?(d) } - if digits.size == 9 - checksum = ((3 * (digits[0] + digits[3] + digits[6])) + - (7 * (digits[1] + digits[4] + digits[7])) + - (digits[2] + digits[5] + digits[8])) % 10 + case digits.size + when 9 + if checksum(digits) == 0 + return routing_number + else + return routing_number[4..8] + routing_number[1..3] + end + when 8 + return routing_number + end + end - return checksum == 0 || CAN_INSTITUTION_NUMBERS.include?(routing_number[1..3]) + # Always return electronic-formatted routing number for Canadian routing numbers, US routing numbers unchanged + def electronic_format_routing_number + digits = routing_number.to_s.split('').map(&:to_i).select { |d| (0..9).cover?(d) } + case digits.size + when 9 + return routing_number + when 8 + return '0' + routing_number[5..7] + routing_number[0..4] end - false end end end diff --git a/lib/active_merchant/billing/credit_card.rb b/lib/active_merchant/billing/credit_card.rb index a29be249041..6982139dad3 100644 --- a/lib/active_merchant/billing/credit_card.rb +++ b/lib/active_merchant/billing/credit_card.rb @@ -32,6 +32,14 @@ module Billing #:nodoc: # * Olimpica # * Creditel # * Confiable + # * Mada + # * BpPlus + # * Passcard + # * Edenred + # * Anda + # * Creditos directos (Tarjeta D) + # * Panal + # * Verve # # For testing purposes, use the 'bogus' credit card brand. This skips the vast majority of # validations, allowing you to focus on your core concerns until you're ready to be more concerned @@ -61,6 +69,8 @@ module Billing #:nodoc: class CreditCard < Model include CreditCardMethods + BRANDS_WITH_SPACES_IN_NUMBER = %w(bp_plus) + class << self # Inherited, but can be overridden w/o changing parent's value attr_accessor :require_verification_value @@ -76,7 +86,7 @@ class << self attr_reader :number def number=(value) - @number = (empty?(value) ? value : value.to_s.gsub(/[^\d]/, '')) + @number = (empty?(value) ? value : filter_number(value)) end # Returns or sets the expiry month for the card. @@ -116,6 +126,14 @@ def number=(value) # * +'olimpica'+ # * +'creditel'+ # * +'confiable'+ + # * +'mada'+ + # * +'bp_plus'+ + # * +'passcard'+ + # * +'edenred'+ + # * +'anda'+ + # * +'tarjeta-d'+ + # * +'panal'+ + # * +'verve'+ # # Or, if you wish to test your implementation, +'bogus'+. # @@ -337,8 +355,21 @@ def emv? icc_data.present? end + def allow_spaces_in_card?(number = nil) + BRANDS_WITH_SPACES_IN_NUMBER.include?(self.class.brand?(self.number || number)) + end + private + def filter_number(value) + regex = if allow_spaces_in_card?(value) + /[^\d ]/ + else + /[^\d]/ + end + value.to_s.gsub(regex, '') + end + def validate_essential_attributes #:nodoc: errors = [] diff --git a/lib/active_merchant/billing/credit_card_methods.rb b/lib/active_merchant/billing/credit_card_methods.rb index f583b728aef..bd9fe0c9197 100644 --- a/lib/active_merchant/billing/credit_card_methods.rb +++ b/lib/active_merchant/billing/credit_card_methods.rb @@ -8,6 +8,7 @@ module CreditCardMethods 'visa' => ->(num) { num =~ /^4\d{12}(\d{3})?(\d{3})?$/ }, 'master' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), MASTERCARD_RANGES) }, 'elo' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), ELO_RANGES) }, + 'cabal' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 8), CABAL_RANGES) }, 'alelo' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), ALELO_RANGES) }, 'discover' => ->(num) { num =~ /^(6011|65\d{2}|64[4-9]\d)\d{12,15}$/ }, 'american_express' => ->(num) { num =~ /^3[47]\d{13}$/ }, @@ -23,10 +24,9 @@ module CreditCardMethods }, 'maestro_no_luhn' => ->(num) { num =~ /^(501080|501081|501082)\d{6,13}$/ }, 'forbrugsforeningen' => ->(num) { num =~ /^600722\d{10}$/ }, - 'sodexo' => ->(num) { num =~ /^(606071|603389|606070|606069|606068|600818)\d{10}$/ }, + 'sodexo' => ->(num) { num =~ /^(606071|603389|606070|606069|606068|600818|505864|505865)\d{10}$/ }, 'alia' => ->(num) { num =~ /^(504997|505878|601030|601073|505874)\d{10}$/ }, 'vr' => ->(num) { num =~ /^(627416|637036)\d{10}$/ }, - 'cabal' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 8), CABAL_RANGES) }, 'unionpay' => ->(num) { (16..19).cover?(num&.size) && in_bin_range?(num.slice(0, 8), UNIONPAY_RANGES) }, 'carnet' => lambda { |num| num&.size == 16 && ( @@ -39,9 +39,20 @@ module CreditCardMethods 'creditel' => ->(num) { num =~ /^601933\d{10}$/ }, 'confiable' => ->(num) { num =~ /^560718\d{10}$/ }, 'synchrony' => ->(num) { num =~ /^700600\d{10}$/ }, - 'routex' => ->(num) { num =~ /^(700676|700678)\d{13}$/ } + 'routex' => ->(num) { num =~ /^(700676|700678)\d{13}$/ }, + 'mada' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), MADA_RANGES) }, + 'bp_plus' => ->(num) { num =~ /^(7050\d\s\d{9}\s\d{3}$|705\d\s\d{8}\s\d{5}$)/ }, + 'passcard' => ->(num) { num =~ /^628026\d{10}$/ }, + 'edenred' => ->(num) { num =~ /^637483\d{10}$/ }, + 'anda' => ->(num) { num =~ /^603199\d{10}$/ }, + 'tarjeta-d' => ->(num) { num =~ /^601828\d{10}$/ }, + 'hipercard' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), HIPERCARD_RANGES) }, + 'panal' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), PANAL_RANGES) }, + 'verve' => ->(num) { (16..19).cover?(num&.size) && in_bin_range?(num.slice(0, 6), VERVE_RANGES) } } + SODEXO_NO_LUHN = ->(num) { num =~ /^(505864|505865)\d{10}$/ } + # http://www.barclaycard.co.uk/business/files/bin_rules.pdf ELECTRON_RANGES = [ [400115], @@ -100,9 +111,9 @@ module CreditCardMethods MAESTRO_BINS = Set.new( %w[ 500057 501018 501043 501045 501047 501049 501051 501072 501075 501083 501087 501089 501095 - 501500 - 501879 502113 502301 503175 - 503645 503670 + 501500 501623 + 501879 502113 502120 502121 502301 + 503175 503337 503645 503670 504310 504338 504363 504533 504587 504620 504639 504656 504738 504781 504910 507001 507002 507004 507082 507090 560014 560565 561033 @@ -113,7 +124,7 @@ module CreditCardMethods 585274 585697 586509 588729 588792 - 589244 589407 589471 589605 589633 589647 589671 + 589244 589407 589471 589605 589633 589647 589671 589916 590043 590206 590263 590265 590278 590361 590362 590379 590393 590590 591235 591420 591481 591620 591770 591948 591994 592024 592161 592184 592186 592201 592384 592393 592528 592566 592704 592735 592879 592884 @@ -124,12 +135,16 @@ module CreditCardMethods 597077 597094 597143 597370 597410 597765 597855 597862 598053 598054 598395 598585 598793 598794 598815 598835 598838 598880 598889 599000 599069 599089 599148 599191 599310 599741 599742 599867 - 601070 601638 + 601070 601452 601628 601638 + 602648 + 603326 603450 603689 604983 606126 - 636380 636422 636502 636639 - 637046 637756 - 639130 639229 + 608710 + 627339 627453 627454 627973 + 636117 636380 636422 636502 636639 + 637046 637529 637568 637600 637756 + 639130 639229 639350 690032] ) @@ -169,7 +184,8 @@ module CreditCardMethods (601256..601276), (601640..601652), (601689..601700), - (602011..602050), + (602011..602048), + [602050], (630400..630499), (639000..639099), (670000..679999), @@ -181,10 +197,10 @@ module CreditCardMethods 506745..506747, 506753..506753, 506774..506778, 509000..509007, 509009..509014, 509020..509030, 509035..509042, 509044..509089, 509091..509101, 509104..509807, 509831..509877, 509897..509900, 509918..509964, 509971..509986, 509995..509999, - 627780..627780, 636368..636368, 650031..650033, 650035..650051, 650057..650081, - 650406..650439, 650485..650504, 650506..650538, 650552..650598, 650720..650727, - 650901..650922, 650928..650928, 650938..650939, 650946..650978, 651652..651704, - 655000..655019, 655021..655057 + 627780..627780, 636297..636298, 636368..636368, 650031..650033, 650035..650051, + 650057..650081, 650406..650439, 650485..650504, 650506..650538, 650552..650598, + 650720..650727, 650901..650922, 650928..650928, 650938..650939, 650946..650978, + 651652..651704, 655000..655019, 655021..655057 ] # Alelo provides BIN ranges by e-mailing them out periodically. @@ -204,7 +220,16 @@ module CreditCardMethods CABAL_RANGES = [ 60420100..60440099, 58965700..58965799, - 60352200..60352299 + 60352200..60352299, + 65027200..65027299, + 65008700..65008700, + 65090000..65090099 + ] + + MADA_RANGES = [ + 504300..504300, 506968..506968, 508160..508160, 585265..585265, 588848..588848, + 588850..588850, 588982..588983, 589005..589005, 589206..589206, 604906..604906, + 605141..605141, 636120..636120, 968201..968209, 968211..968211 ] NARANJA_RANGES = [ @@ -221,6 +246,50 @@ module CreditCardMethods 3528..3589, 3088..3094, 3096..3102, 3112..3120, 3158..3159, 3337..3349 ] + HIPERCARD_RANGES = [ + 384100..384100, 384140..384140, 384160..384160, 606282..606282, 637095..637095, + 637568..637568, 637599..637599, 637609..637609, 637612..637612 + ] + + PANAL_RANGES = [[602049]] + + VERVE_RANGES = [ + [506099], + [506101], + [506103], + (506111..506114), + [506116], + [506118], + [506124], + [506127], + [506130], + (506132..506139), + [506141], + [506144], + (506146..506152), + (506154..506161), + (506163..506164), + [506167], + (506169..506198), + (507865..507866), + (507868..507872), + (507874..507899), + (507901..507909), + (507911..507919), + [507921], + (507923..507925), + (507927..507962), + [507964], + [627309], + [627903], + [628051], + [636625], + [637058], + [637634], + [639245], + [639383] + ] + def self.included(base) base.extend(ClassMethods) end @@ -295,7 +364,7 @@ module ClassMethods def valid_number?(number) valid_test_mode_card_number?(number) || valid_card_number_length?(number) && - valid_card_number_characters?(number) && + valid_card_number_characters?(brand?(number), number) && valid_by_algorithm?(brand?(number), number) end @@ -362,8 +431,9 @@ def valid_card_number_length?(number) #:nodoc: number.length >= 12 end - def valid_card_number_characters?(number) #:nodoc: + def valid_card_number_characters?(brand, number) #:nodoc: return false if number.nil? + return number =~ /\A[0-9 ]+\Z/ if brand == 'bp_plus' !number.match(/\D/) end @@ -373,20 +443,28 @@ def valid_test_mode_card_number?(number) #:nodoc: %w[1 2 3 success failure error].include?(number) end + def sodexo_no_luhn?(numbers) + SODEXO_NO_LUHN.call(numbers) + end + def valid_by_algorithm?(brand, numbers) #:nodoc: case brand when 'naranja' valid_naranja_algo?(numbers) when 'creditel' valid_creditel_algo?(numbers) - when 'alia', 'confiable', 'maestro_no_luhn' + when 'alia', 'confiable', 'maestro_no_luhn', 'anda', 'tarjeta-d', 'hipercard' true + when 'sodexo' + sodexo_no_luhn?(numbers) ? true : valid_luhn?(numbers) + when 'bp_plus', 'passcard', 'edenred' + valid_luhn_non_zero_check_digit?(numbers) else valid_luhn?(numbers) end end - ODD_LUHN_VALUE = { + BYTES_TO_DIGITS = { 48 => 0, 49 => 1, 50 => 2, @@ -400,7 +478,7 @@ def valid_by_algorithm?(brand, numbers) #:nodoc: nil => 0 }.freeze - EVEN_LUHN_VALUE = { + BYTES_TO_DIGITS_DOUBLED = { 48 => 0, # 0 * 2 49 => 2, # 1 * 2 50 => 4, # 2 * 2 @@ -420,19 +498,40 @@ def valid_luhn?(numbers) #:nodoc: sum = 0 odd = true - numbers.reverse.bytes.each do |number| + numbers.reverse.bytes.each do |bytes| if odd odd = false - sum += ODD_LUHN_VALUE[number] + sum += BYTES_TO_DIGITS[bytes] else odd = true - sum += EVEN_LUHN_VALUE[number] + sum += BYTES_TO_DIGITS_DOUBLED[bytes] end end sum % 10 == 0 end + def valid_luhn_with_check_digit?(numbers, check_digit) + sum = 0 + + doubler = true + + numbers.reverse.bytes.each do |bytes| + doubler ? sum += BYTES_TO_DIGITS_DOUBLED[bytes] : sum += BYTES_TO_DIGITS[bytes] + doubler = !doubler + end + + (10 - (sum % 10)) % 10 == check_digit.to_i + end + + def valid_luhn_non_zero_check_digit?(numbers) + return valid_luhn?(numbers.delete(' ')) if numbers[5] == ' ' + + check_digit = numbers[-1] + luhn_payload = numbers.delete(' ').chop + valid_luhn_with_check_digit?(luhn_payload, check_digit) + end + # Checks the validity of a card number by use of specific algorithms def valid_naranja_algo?(numbers) #:nodoc: num_array = numbers.to_s.chars.map(&:to_i) diff --git a/lib/active_merchant/billing/gateway.rb b/lib/active_merchant/billing/gateway.rb index 063f65b9b9d..2cbeca869a1 100644 --- a/lib/active_merchant/billing/gateway.rb +++ b/lib/active_merchant/billing/gateway.rb @@ -315,6 +315,15 @@ def split_names(full_name) [first_name, last_name] end + def split_address(full_address) + address_parts = (full_address || '').split + return [nil, nil] if address_parts.size == 0 + + number = address_parts.shift + street = address_parts.join(' ') + [number, street] + end + def requires!(hash, *params) params.each do |param| if param.is_a?(Array) diff --git a/lib/active_merchant/billing/gateways/adyen.rb b/lib/active_merchant/billing/gateways/adyen.rb index dd02c57ecc2..43cd1e9029e 100644 --- a/lib/active_merchant/billing/gateways/adyen.rb +++ b/lib/active_merchant/billing/gateways/adyen.rb @@ -17,12 +17,16 @@ class AdyenGateway < Gateway self.homepage_url = 'https://www.adyen.com/' self.display_name = 'Adyen' - PAYMENT_API_VERSION = 'v64' - RECURRING_API_VERSION = 'v49' + PAYMENT_API_VERSION = 'v68' + RECURRING_API_VERSION = 'v68' STANDARD_ERROR_CODE_MAPPING = { + '0' => STANDARD_ERROR_CODE[:processing_error], + '10' => STANDARD_ERROR_CODE[:config_error], + '100' => STANDARD_ERROR_CODE[:invalid_amount], '101' => STANDARD_ERROR_CODE[:incorrect_number], '103' => STANDARD_ERROR_CODE[:invalid_cvc], + '104' => STANDARD_ERROR_CODE[:incorrect_address], '131' => STANDARD_ERROR_CODE[:incorrect_address], '132' => STANDARD_ERROR_CODE[:incorrect_address], '133' => STANDARD_ERROR_CODE[:incorrect_address], @@ -62,6 +66,10 @@ def authorize(money, payment, options = {}) add_recurring_contract(post, options) add_network_transaction_reference(post, options) add_application_info(post, options) + add_level_2_data(post, options) + add_level_3_data(post, options) + add_data_airline(post, options) + add_data_lodging(post, options) commit('authorise', post, options) end @@ -71,6 +79,7 @@ def capture(money, authorization, options = {}) add_reference(post, authorization, options) add_splits(post, options) add_network_transaction_reference(post, options) + add_shopper_statement(post, options) commit('capture', post, options) end @@ -84,12 +93,28 @@ def refund(money, authorization, options = {}) end def credit(money, payment, options = {}) - action = 'refundWithData' + action = options[:payout] ? 'payout' : 'refundWithData' post = init_post(options) add_invoice(post, money, options) add_payment(post, payment, options, action) add_shopper_reference(post, options) add_network_transaction_reference(post, options) + + if action == 'payout' + add_shopper_interaction(post, payment, options) + add_fraud_offset(post, options) + add_fund_source(post, options) + add_recurring_contract(post, options) + add_shopper_data(post, payment, options) + + if (address = options[:billing_address] || options[:address]) && address[:country] + add_billing_address(post, options, address) + end + + post[:dateOfBirth] = options[:date_of_birth] if options[:date_of_birth] + post[:nationality] = options[:nationality] if options[:nationality] + end + commit(action, post, options) end @@ -117,7 +142,7 @@ def store(credit_card, options = {}) add_extra_data(post, credit_card, options) add_stored_credentials(post, credit_card, options) add_address(post, options) - + add_network_transaction_reference(post, options) options[:recurring_contract_type] ||= 'RECURRING' add_recurring_contract(post, options) @@ -216,17 +241,34 @@ def scrub(transcript) NETWORK_TOKENIZATION_CARD_SOURCE = { 'apple_pay' => 'applepay', 'android_pay' => 'androidpay', - 'google_pay' => 'paywithgoogle' + 'google_pay' => 'googlepay' } def add_extra_data(post, payment, options) post[:telephoneNumber] = (options[:billing_address][:phone_number] if options.dig(:billing_address, :phone_number)) || (options[:billing_address][:phone] if options.dig(:billing_address, :phone)) || '' - post[:fraudOffset] = options[:fraud_offset] if options[:fraud_offset] post[:selectedBrand] = options[:selected_brand] if options[:selected_brand] post[:selectedBrand] ||= NETWORK_TOKENIZATION_CARD_SOURCE[payment.source.to_s] if payment.is_a?(NetworkTokenizationCreditCard) post[:deliveryDate] = options[:delivery_date] if options[:delivery_date] post[:merchantOrderReference] = options[:merchant_order_reference] if options[:merchant_order_reference] post[:captureDelayHours] = options[:capture_delay_hours] if options[:capture_delay_hours] + post[:deviceFingerprint] = options[:device_fingerprint] if options[:device_fingerprint] + post[:shopperIP] = options[:shopper_ip] || options[:ip] if options[:shopper_ip] || options[:ip] + post[:shopperStatement] = options[:shopper_statement] if options[:shopper_statement] + post[:store] = options[:store] if options[:store] + + add_shopper_data(post, payment, options) + add_additional_data(post, payment, options) + add_risk_data(post, options) + add_shopper_reference(post, options) + add_merchant_data(post, options) + add_fraud_offset(post, options) + end + + def add_fraud_offset(post, options) + post[:fraudOffset] = options[:fraud_offset] if options[:fraud_offset] + end + + def add_additional_data(post, payment, options) post[:additionalData] ||= {} post[:additionalData][:overwriteBrand] = normalize(options[:overwrite_brand]) if options[:overwrite_brand] post[:additionalData][:customRoutingFlag] = options[:custom_routing_flag] if options[:custom_routing_flag] @@ -235,20 +277,135 @@ def add_extra_data(post, payment, options) post[:additionalData][:adjustAuthorisationData] = options[:adjust_authorisation_data] if options[:adjust_authorisation_data] post[:additionalData][:industryUsage] = options[:industry_usage] if options[:industry_usage] post[:additionalData][:RequestedTestAcquirerResponseCode] = options[:requested_test_acquirer_response_code] if options[:requested_test_acquirer_response_code] && test? - post[:deviceFingerprint] = options[:device_fingerprint] if options[:device_fingerprint] - add_shopper_data(post, options) - add_risk_data(post, options) - add_shopper_reference(post, options) - add_merchant_data(post, options) + post[:additionalData][:updateShopperStatement] = options[:update_shopper_statement] if options[:update_shopper_statement] end - def add_shopper_data(post, options) - post[:shopperEmail] = options[:email] if options[:email] - post[:shopperEmail] = options[:shopper_email] if options[:shopper_email] - post[:shopperIP] = options[:ip] if options[:ip] - post[:shopperIP] = options[:shopper_ip] if options[:shopper_ip] - post[:shopperStatement] = options[:shopper_statement] if options[:shopper_statement] - post[:additionalData][:updateShopperStatement] = options[:update_shopper_statement] if options[:update_shopper_statement] + def extract_and_transform(mapper, from) + mapper.each_with_object({}) do |key_map, hsh| + key, item_key = key_map[0], key_map[1] + hsh[key] = from[item_key.to_sym] + end + end + + def add_level_2_data(post, options) + return unless options[:level_2_data].present? + + mapper = { + "enhancedSchemeData.totalTaxAmount": 'total_tax_amount', + "enhancedSchemeData.customerReference": 'customer_reference' + } + post[:additionalData].merge!(extract_and_transform(mapper, options[:level_2_data])) + end + + def add_level_3_data(post, options) + return unless options[:level_3_data].present? + + mapper = { "enhancedSchemeData.freightAmount": 'freight_amount', + "enhancedSchemeData.destinationStateProvinceCode": 'destination_state_province_code', + "enhancedSchemeData.shipFromPostalCode": 'ship_from_postal_code', + "enhancedSchemeData.orderDate": 'order_date', + "enhancedSchemeData.destinationPostalCode": 'destination_postal_code', + "enhancedSchemeData.destinationCountryCode": 'destination_country_code', + "enhancedSchemeData.dutyAmount": 'duty_amount' } + + post[:additionalData].merge!(extract_and_transform(mapper, options[:level_3_data])) + + item_detail_keys = %w[description product_code quantity unit_of_measure unit_price discount_amount total_amount commodity_code] + if options[:level_3_data][:items].present? + options[:level_3_data][:items].last(9).each.with_index(1) do |item, index| + mapper = item_detail_keys.each_with_object({}) do |key, hsh| + hsh["enhancedSchemeData.itemDetailLine#{index}.#{key.camelize(:lower)}"] = key + end + post[:additionalData].merge!(extract_and_transform(mapper, item)) + end + end + post[:additionalData].compact! + end + + def add_data_airline(post, options) + return unless options[:additional_data_airline] + + mapper = %w[ + agency_invoice_number + agency_plan_name + airline_code + airline_designator_code + boarding_fee + computerized_reservation_system + customer_reference_number + document_type + flight_date + ticket_issue_address + ticket_number + travel_agency_code + travel_agency_name + passenger_name + ].each_with_object({}) { |value, hash| hash["airline.#{value}"] = value } + + post[:additionalData].merge!(extract_and_transform(mapper, options[:additional_data_airline])) + + if options[:additional_data_airline][:leg].present? + leg_data = %w[ + carrier_code + class_of_travel + date_of_travel + depart_airport + depart_tax + destination_code + fare_base_code + flight_number + stop_over_code + ].each_with_object({}) { |value, hash| hash["airline.leg.#{value}"] = value } + + post[:additionalData].merge!(extract_and_transform(leg_data, options[:additional_data_airline][:leg])) + end + + if options[:additional_data_airline][:passenger].present? + passenger_data = %w[ + date_of_birth + first_name + last_name + telephone_number + traveller_type + ].each_with_object({}) { |value, hash| hash["airline.passenger.#{value}"] = value } + + post[:additionalData].merge!(extract_and_transform(passenger_data, options[:additional_data_airline][:passenger])) + end + post[:additionalData].compact! + end + + def add_data_lodging(post, options) + return unless options[:additional_data_lodging] + + mapper = { + 'lodging.checkInDate': 'check_in_date', + 'lodging.checkOutDate': 'check_out_date', + 'lodging.customerServiceTollFreeNumber': 'customer_service_toll_free_number', + 'lodging.fireSafetyActIndicator': 'fire_safety_act_indicator', + 'lodging.folioCashAdvances': 'folio_cash_advances', + 'lodging.folioNumber': 'folio_number', + 'lodging.foodBeverageCharges': 'food_beverage_charges', + 'lodging.noShowIndicator': 'no_show_indicator', + 'lodging.prepaidExpenses': 'prepaid_expenses', + 'lodging.propertyPhoneNumber': 'property_phone_number', + 'lodging.room1.numberOfNights': 'number_of_nights', + 'lodging.room1.rate': 'rate', + 'lodging.totalRoomTax': 'total_room_tax', + 'lodging.totalTax': 'totalTax', + 'travelEntertainmentAuthData.duration': 'duration', + 'travelEntertainmentAuthData.market': 'market' + } + + post[:additionalData].merge!(extract_and_transform(mapper, options[:additional_data_lodging])) + post[:additionalData].compact! + end + + def add_shopper_statement(post, options) + return unless options[:shopper_statement] + + post[:additionalData] = { + shopperStatement: options[:shopper_statement] + } end def add_merchant_data(post, options) @@ -336,8 +493,8 @@ def add_recurring_processing_model(post, options) def add_address(post, options) if address = options[:shipping_address] post[:deliveryAddress] = {} - post[:deliveryAddress][:street] = address[:address1] || 'NA' - post[:deliveryAddress][:houseNumberOrName] = address[:address2] || 'NA' + post[:deliveryAddress][:street] = options[:address_override] == true ? address[:address2] : address[:address1] || 'NA' + post[:deliveryAddress][:houseNumberOrName] = options[:address_override] == true ? address[:address1] : address[:address2] || 'NA' post[:deliveryAddress][:postalCode] = address[:zip] if address[:zip] post[:deliveryAddress][:city] = address[:city] || 'NA' post[:deliveryAddress][:stateOrProvince] = get_state(address) @@ -346,16 +503,21 @@ def add_address(post, options) return unless post[:bankAccount]&.kind_of?(Hash) || post[:card]&.kind_of?(Hash) if (address = options[:billing_address] || options[:address]) && address[:country] - post[:billingAddress] = {} - post[:billingAddress][:street] = address[:address1] || 'NA' - post[:billingAddress][:houseNumberOrName] = address[:address2] || 'NA' - post[:billingAddress][:postalCode] = address[:zip] if address[:zip] - post[:billingAddress][:city] = address[:city] || 'NA' - post[:billingAddress][:stateOrProvince] = get_state(address) - post[:billingAddress][:country] = address[:country] if address[:country] + add_billing_address(post, options, address) end end + def add_billing_address(post, options, address) + post[:billingAddress] = {} + post[:billingAddress][:street] = options[:address_override] == true ? address[:address2] : address[:address1] || 'NA' + post[:billingAddress][:houseNumberOrName] = options[:address_override] == true ? address[:address1] : address[:address2] || 'NA' + post[:billingAddress][:postalCode] = address[:zip] if address[:zip] + post[:billingAddress][:city] = address[:city] || 'NA' + post[:billingAddress][:stateOrProvince] = get_state(address) + post[:billingAddress][:country] = address[:country] if address[:country] + post[:telephoneNumber] = address[:phone_number] || address[:phone] || '' + end + def get_state(address) address[:state] && !address[:state].blank? ? address[:state] : 'NA' end @@ -387,7 +549,7 @@ def add_payment(post, payment, options, action = nil) elsif payment.is_a?(Check) add_bank_account(post, payment, options, action) else - add_mpi_data_for_network_tokenization_card(post, payment) if payment.is_a?(NetworkTokenizationCreditCard) + add_mpi_data_for_network_tokenization_card(post, payment, options) if payment.is_a?(NetworkTokenizationCreditCard) add_card(post, payment) end end @@ -396,7 +558,7 @@ def add_bank_account(post, bank_account, options, action) bank = { bankAccountNumber: bank_account.account_number, ownerName: bank_account.name, - countryCode: options[:billing_address][:country] + countryCode: options[:billing_address].try(:[], :country) } action == 'refundWithData' ? bank[:iban] = bank_account.routing_number : bank[:bankLocationId] = bank_account.routing_number @@ -420,6 +582,17 @@ def add_card(post, credit_card) post[:card] = card end + def add_shopper_data(post, payment, options) + if payment && !payment.is_a?(String) + post[:shopperName] = {} + post[:shopperName][:firstName] = payment.first_name + post[:shopperName][:lastName] = payment.last_name + end + + post[:shopperEmail] = options[:email] if options[:email] + post[:shopperEmail] = options[:shopper_email] if options[:shopper_email] + end + def capture_options(options) return options.merge(idempotency_key: "#{options[:idempotency_key]}-cap") if options[:idempotency_key] @@ -438,7 +611,9 @@ def add_reference(post, authorization, options = {}) post[:originalReference] = original_reference end - def add_mpi_data_for_network_tokenization_card(post, payment) + def add_mpi_data_for_network_tokenization_card(post, payment, options) + return if options[:skip_mpi_data] == 'Y' + post[:mpiData] = {} post[:mpiData][:authenticationResponse] = 'Y' post[:mpiData][:cavv] = payment.payment_cryptogram @@ -449,11 +624,12 @@ def add_mpi_data_for_network_tokenization_card(post, payment) def add_recurring_contract(post, options = {}) return unless options[:recurring_contract_type] - recurring = { - contract: options[:recurring_contract_type] - } - - post[:recurring] = recurring + post[:recurring] = {} + post[:recurring][:contract] = options[:recurring_contract_type] + post[:recurring][:recurringDetailName] = options[:recurring_detail_name] if options[:recurring_detail_name] + post[:recurring][:recurringExpiry] = options[:recurring_expiry] if options[:recurring_expiry] + post[:recurring][:recurringFrequency] = options[:recurring_frequency] if options[:recurring_frequency] + post[:recurring][:tokenService] = options[:token_service] if options[:token_service] end def add_application_info(post, options) @@ -549,6 +725,23 @@ def add_3ds2_authenticated_data(post, options) } end + def add_fund_source(post, options) + return unless fund_source = options[:fund_source] + + post[:fundSource] = {} + post[:fundSource][:additionalData] = fund_source[:additional_data] if fund_source[:additional_data] + + if fund_source[:first_name] && fund_source[:last_name] + post[:fundSource][:shopperName] = {} + post[:fundSource][:shopperName][:firstName] = fund_source[:first_name] + post[:fundSource][:shopperName][:lastName] = fund_source[:last_name] + end + + if (address = fund_source[:billing_address]) + add_billing_address(post[:fundSource], options, address) + end + end + def parse(body) return {} if body.blank? @@ -567,7 +760,7 @@ def commit(action, parameters, options) success = success_from(action, response, options) Response.new( success, - message_from(action, response), + message_from(action, response, options), response, authorization: authorization_from(action, parameters, response), test: test?, @@ -587,8 +780,14 @@ def cvv_result_from(response) end def endpoint(action) - recurring = %w(disable storeToken).include?(action) - recurring ? "Recurring/#{RECURRING_API_VERSION}/#{action}" : "Payment/#{PAYMENT_API_VERSION}/#{action}" + case action + when 'disable', 'storeToken' + "Recurring/#{RECURRING_API_VERSION}/#{action}" + when 'payout' + "Payout/#{PAYMENT_API_VERSION}/#{action}" + else + "Payment/#{PAYMENT_API_VERSION}/#{action}" + end end def url(action) @@ -616,7 +815,7 @@ def request_headers(options) def success_from(action, response, options) if %w[RedirectShopper ChallengeShopper].include?(response.dig('resultCode')) && !options[:execute_threed] && !options[:threed_dynamic] - response['refusalReason'] = 'Received unexpected 3DS authentication response. Use the execute_threed and/or threed_dynamic options to initiate a proper 3DS flow.' + response['refusalReason'] = 'Received unexpected 3DS authentication response, but a 3DS initiation flag was not included in the request.' return false end case action.to_s @@ -632,18 +831,37 @@ def success_from(action, response, options) response['response'] == '[detail-successfully-disabled]' when 'refundWithData' response['resultCode'] == 'Received' + when 'payout' + return false unless response['resultCode'] && response['authCode'] + + %[AuthenticationFinished Authorised Received].include?(response['resultCode']) else false end end - def message_from(action, response) - return authorize_message_from(response) if %w(authorise authorise3d authorise3ds2).include?(action.to_s) + def message_from(action, response, options = {}) + case action.to_s + when 'authorise', 'authorise3d', 'authorise3ds2' + authorize_message_from(response, options) + when 'payout' + response['refusalReason'] || response['resultCode'] || response['message'] + else + response['response'] || response['message'] || response['result'] || response['resultCode'] + end + end + + def authorize_message_from(response, options = {}) + return raw_authorize_error_message(response) if options[:raw_error_message] - response['response'] || response['message'] || response['result'] || response['resultCode'] + if response['refusalReason'] && response['additionalData'] && (response['additionalData']['merchantAdviceCode'] || response['additionalData']['refusalReasonRaw']) + "#{response['refusalReason']} | #{response['additionalData']['merchantAdviceCode'] || response['additionalData']['refusalReasonRaw']}" + else + response['refusalReason'] || response['resultCode'] || response['message'] || response['result'] + end end - def authorize_message_from(response) + def raw_authorize_error_message(response) if response['refusalReason'] && response['additionalData'] && response['additionalData']['refusalReasonRaw'] "#{response['refusalReason']} | #{response['additionalData']['refusalReasonRaw']}" else diff --git a/lib/active_merchant/billing/gateways/airwallex.rb b/lib/active_merchant/billing/gateways/airwallex.rb index 085cb6b20e9..fd1e06f427d 100644 --- a/lib/active_merchant/billing/gateways/airwallex.rb +++ b/lib/active_merchant/billing/gateways/airwallex.rb @@ -36,17 +36,15 @@ def initialize(options = {}) end def purchase(money, card, options = {}) - requires!(options, :return_url) - payment_intent_id = create_payment_intent(money, options) post = { 'request_id' => request_id(options), - 'merchant_order_id' => merchant_order_id(options), - 'return_url' => options[:return_url] + 'merchant_order_id' => merchant_order_id(options) } add_card(post, card, options) add_descriptor(post, options) add_stored_credential(post, options) + add_return_url(post, options) post['payment_method_options'] = { 'card' => { 'auto_capture' => false } } if authorization_only?(options) add_three_ds(post, options) @@ -114,15 +112,19 @@ def scrub(transcript) private def request_id(options) - options[:request_id] || generate_timestamp + options[:request_id] || generate_uuid end def merchant_order_id(options) - options[:merchant_order_id] || options[:order_id] || generate_timestamp + options[:merchant_order_id] || options[:order_id] || generate_uuid + end + + def add_return_url(post, options) + post[:return_url] = options[:return_url] if options[:return_url] end - def generate_timestamp - (Time.now.to_f.round(2) * 100).to_i.to_s + def generate_uuid + SecureRandom.uuid end def setup_access_token @@ -137,7 +139,13 @@ def setup_access_token def build_request_url(action, id = nil) base_url = (test? ? test_url : live_url) - base_url + ENDPOINTS[action].to_s % { id: id } + endpoint = ENDPOINTS[action].to_s + endpoint = id.present? ? endpoint % { id: id } : endpoint + base_url + endpoint + end + + def add_referrer_data(post) + post[:referrer_data] = { type: 'spreedly' } end def create_payment_intent(money, options = {}) @@ -145,8 +153,10 @@ def create_payment_intent(money, options = {}) add_invoice(post, money, options) add_order(post, options) post[:request_id] = "#{request_id(options)}_setup" - post[:merchant_order_id] = "#{merchant_order_id(options)}_setup" + post[:merchant_order_id] = merchant_order_id(options) + add_referrer_data(post) add_descriptor(post, options) + post['payment_method_options'] = { 'card' => { 'risk_control' => { 'three_ds_action' => 'SKIP_3DS' } } } if options[:skip_3ds] response = commit(:setup, post) raise ArgumentError.new(response.message) unless response.success? @@ -313,10 +323,6 @@ def parse(body) def commit(action, post, id = nil) url = build_request_url(action, id) - # MIT txn w/ no NTID should fail and the gateway should say so - # Logic to prevent sending anything on test transactions - # if Airwallex changes the value it won't matter - # Passing nothing still doesn't test the happy path post_headers = { 'Authorization' => "Bearer #{@access_token}", 'Content-Type' => 'application/json' } response = parse(ssl_post(url, post_data(post), post_headers)) diff --git a/lib/active_merchant/billing/gateways/alelo.rb b/lib/active_merchant/billing/gateways/alelo.rb new file mode 100644 index 00000000000..fd1cfc11a5f --- /dev/null +++ b/lib/active_merchant/billing/gateways/alelo.rb @@ -0,0 +1,256 @@ +require 'jose' + +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class AleloGateway < Gateway + class_attribute :prelive_url + + self.test_url = 'https://sandbox-api.alelo.com.br/alelo/sandbox/' + self.live_url = 'https://api.alelo.com.br/alelo/prd/' + self.prelive_url = 'https://api.homologacaoalelo.com.br/alelo/uat/' + + self.supported_countries = ['BR'] + self.default_currency = 'BRL' + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'https://www.alelo.com.br' + self.display_name = 'Alelo' + + def initialize(options = {}) + requires!(options, :client_id, :client_secret) + super + end + + def purchase(money, payment, options = {}) + post = {} + add_order(post, options) + add_amount(post, money) + add_payment(post, payment) + add_geolocation(post, options) + add_extra_data(post, options) + + commit('capture/transaction', post, options) + end + + def refund(money, authorization, options = {}) + request_id = authorization.split('#').first + options[:http] = { method: :put, prevent_encrypt: true } + commit('capture/transaction/refund', { requestId: request_id }, options, :put) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + force_utf8(transcript.encode). + gsub(%r((Authorization: Bearer )[\w -]+), '\1[FILTERED]'). + gsub(%r((client_id=|Client-Id:)[\w -]+), '\1[FILTERED]\2'). + gsub(%r((client_secret=|Client-Secret:)[\w -]+), '\1[FILTERED]\2'). + gsub(%r((access_token\":\")[^\"]*), '\1[FILTERED]'). + gsub(%r((publicKey\":\")[^\"]*), '\1[FILTERED]') + end + + private + + def force_utf8(string) + return nil unless string + + # binary = string.encode('BINARY', invalid: :replace, undef: :replace, replace: '?') + string.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') + end + + def add_amount(post, money) + post[:amount] = amount(money).to_f + end + + def add_order(post, options) + post[:requestId] = options[:order_id] + end + + def add_extra_data(post, options) + post.merge!({ + establishmentCode: options[:establishment_code], + playerIdentification: options[:player_identification], + captureType: '3', # send fixed value 3 to ecommerce + subMerchantCode: options[:sub_merchant_mcc], + externalTraceNumber: options[:external_trace_number] + }.compact) + end + + def add_geolocation(post, options) + return if options[:geo_latitude].blank? || options[:geo_longitude].blank? + + post.merge!(geolocation: { + latitude: options[:geo_latitude], + longitude: options[:geo_longitude] + }) + end + + def add_payment(post, payment) + post.merge!({ + cardNumber: payment.number, + cardholderName: payment.name, + expirationMonth: payment.month, + expirationYear: format(payment.year, :two_digits).to_i, + securityCode: payment.verification_value + }) + end + + def fetch_access_token + params = { + grant_type: 'client_credentials', + client_id: @options[:client_id], + client_secret: @options[:client_secret], + scope: '/capture' + } + + headers = { + 'Accept' => 'application/json', + 'Content-Type' => 'application/x-www-form-urlencoded' + } + + parsed = parse(ssl_post(url('captura-oauth-provider/oauth/token'), post_data(params), headers)) + Response.new(true, parsed[:access_token], parsed) + end + + def remote_encryption_key(access_token) + response = parse(ssl_get(url('capture/key'), request_headers(access_token))) + Response.new(true, response[:publicKey], response) + end + + def ensure_credentials(try_again = true) + multiresp = MultiResponse.new + access_token = @options[:access_token] + key = @options[:encryption_key] + uuid = @options[:encryption_uuid] + + if access_token.blank? + multiresp.process { fetch_access_token } + access_token = multiresp.message + key = nil + uuid = nil + end + + if key.blank? + multiresp.process { remote_encryption_key(access_token) } + key = multiresp.message + uuid = multiresp.params['uuid'] + end + + { + key: key, + uuid: uuid, + access_token: access_token, + multiresp: multiresp.responses.present? ? multiresp : nil + } + rescue ResponseError => e + # retry to generate a new access_token when the provided one is expired + raise e unless try_again && %w(401 404).include?(e.response.code) && @options[:access_token].present? + + @options.delete(:access_token) + @options.delete(:encryption_key) + ensure_credentials false + end + + def encrypt_payload(body, credentials, options) + key = OpenSSL::PKey::RSA.new(Base64.decode64(credentials[:key])) + jwk = JOSE::JWK.from_key(key) + alg_enc = { 'alg' => 'RSA-OAEP-256', 'enc' => 'A128CBC-HS256' } + + token = JOSE::JWE.block_encrypt(jwk, body.to_json, alg_enc).compact + + encrypted_body = { + token: token, + uuid: credentials[:uuid] + } + + encrypted_body.to_json + end + + def parse(body) + JSON.parse(body, symbolize_names: true) + end + + def post_data(params) + params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') + end + + def commit(action, body, options, try_again = true) + credentials = ensure_credentials + payload = encrypt_payload(body, credentials, options) + + if options.dig :http, :method + payload = body.to_json if options.dig :http, :prevent_encrypt + response = parse ssl_request(options[:http][:method], url(action), payload, request_headers(credentials[:access_token])) + else + response = parse ssl_post(url(action), payload, request_headers(credentials[:access_token])) + end + + resp = Response.new( + success_from(action, response), + message_from(response), + response, + authorization: authorization_from(response, options), + test: test? + ) + + return resp unless credentials[:multiresp].present? + + multiresp = credentials[:multiresp] + resp.params.merge!({ + 'access_token' => credentials[:access_token], + 'encryption_key' => credentials[:key], + 'encryption_uuid' => credentials[:uuid] + }) + multiresp.process { resp } + + multiresp + rescue ActiveMerchant::ResponseError => e + # Retry on a possible expired encryption key + if try_again && %w(401 404).include?(e.response.code) && @options[:encryption_key].present? + @options.delete(:encryption_key) + commit(action, body, options, false) + else + res = parse(e.response.body) + Response.new(false, res[:messageUser] || res[:error], res, test: test?) + end + end + + def success_from(action, response) + case action + when 'capture/transaction/refund' + response[:status] == 'ESTORNADA' + when 'capture/transaction' + response[:status] == 'CONFIRMADA' + else + false + end + end + + def message_from(response) + response[:messages] || response[:messageUser] + end + + def authorization_from(response, options) + [response[:requestId]].join('#') + end + + def url(action) + return prelive_url if @options[:url_override] == 'prelive' + + "#{test? ? test_url : live_url}#{action}" + end + + def request_headers(access_token) + { + 'Accept' => 'application/json', + 'X-IBM-Client-Id' => @options[:client_id], + 'X-IBM-Client-Secret' => @options[:client_secret], + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer #{access_token}" + } + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/authorize_net.rb b/lib/active_merchant/billing/gateways/authorize_net.rb index 2669ed7f467..c3a36a62cf2 100644 --- a/lib/active_merchant/billing/gateways/authorize_net.rb +++ b/lib/active_merchant/billing/gateways/authorize_net.rb @@ -90,7 +90,6 @@ class AuthorizeNetGateway < Gateway }.freeze APPLE_PAY_DATA_DESCRIPTOR = 'COMMON.APPLE.INAPP.PAYMENT' - PAYMENT_METHOD_NOT_SUPPORTED_ERROR = '155' INELIGIBLE_FOR_ISSUING_CREDIT_ERROR = '54' @@ -176,11 +175,29 @@ def credit(amount, payment, options = {}) end end - def verify(credit_card, options = {}) + def verify(payment_method, options = {}) + amount = amount_for_verify(options) + MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options) } + r.process { authorize(amount, payment_method, options) } + r.process(:ignore_result) { void(r.authorization, options) } unless amount == 0 end + rescue ArgumentError => e + Response.new(false, e.message) + end + + def amount_for_verify(options) + return 100 unless options[:verify_amount].present? + + amount = options[:verify_amount] + raise ArgumentError.new 'verify_amount value must be an integer' unless amount.is_a?(Integer) && !amount.negative? || amount.is_a?(String) && amount.match?(/^\d+$/) && !amount.to_i.negative? + raise ArgumentError.new 'Billing address including zip code is required for a 0 amount verify' if amount.to_i.zero? && !validate_billing_address_values?(options) + + amount.to_i + end + + def validate_billing_address_values?(options) + options.dig(:billing_address, :zip).present? && options.dig(:billing_address, :address1).present? end def store(credit_card, options = {}) @@ -345,7 +362,7 @@ def normal_refund(amount, authorization, options) xml.accountType(options[:account_type]) xml.routingNumber(options[:routing_number]) xml.accountNumber(options[:account_number]) - xml.nameOnAccount("#{options[:first_name]} #{options[:last_name]}") + xml.nameOnAccount(truncate("#{options[:first_name]} #{options[:last_name]}", 22)) end else xml.creditCard do @@ -598,6 +615,7 @@ def add_billing_address(xml, payment_source, options) first_name, last_name = names_from(payment_source, address, options) state = state_from(address, options) full_address = "#{address[:address1]} #{address[:address2]}".strip + phone = address[:phone] || address[:phone_number] || '' xml.firstName(truncate(first_name, 50)) unless empty?(first_name) xml.lastName(truncate(last_name, 50)) unless empty?(last_name) @@ -607,7 +625,7 @@ def add_billing_address(xml, payment_source, options) xml.state(truncate(state, 40)) xml.zip(truncate((address[:zip] || options[:zip]), 20)) xml.country(truncate(address[:country], 60)) - xml.phoneNumber(truncate(address[:phone], 25)) unless empty?(address[:phone]) + xml.phoneNumber(truncate(phone, 25)) unless empty?(phone) xml.faxNumber(truncate(address[:fax], 25)) unless empty?(address[:fax]) end end diff --git a/lib/active_merchant/billing/gateways/authorize_net_arb.rb b/lib/active_merchant/billing/gateways/authorize_net_arb.rb index 5bcf08b8107..85a5d6c4b10 100644 --- a/lib/active_merchant/billing/gateways/authorize_net_arb.rb +++ b/lib/active_merchant/billing/gateways/authorize_net_arb.rb @@ -393,9 +393,13 @@ def recurring_commit(action, request) test_mode = test? || message =~ /Test Mode/ success = response[:result_code] == 'Ok' - Response.new(success, message, response, + Response.new( + success, + message, + response, test: test_mode, - authorization: response[:subscription_id]) + authorization: response[:subscription_id] + ) end def recurring_parse(action, xml) diff --git a/lib/active_merchant/billing/gateways/axcessms.rb b/lib/active_merchant/billing/gateways/axcessms.rb index b3e113338a6..eff4b112086 100644 --- a/lib/active_merchant/billing/gateways/axcessms.rb +++ b/lib/active_merchant/billing/gateways/axcessms.rb @@ -71,9 +71,13 @@ def commit(paymentcode, money, payment, options) message = "#{response[:reason]} - #{response[:return]}" authorization = response[:unique_id] - Response.new(success, message, response, + Response.new( + success, + message, + response, authorization: authorization, - test: (response[:mode] != 'LIVE')) + test: (response[:mode] != 'LIVE') + ) end def parse(body) diff --git a/lib/active_merchant/billing/gateways/banwire.rb b/lib/active_merchant/billing/gateways/banwire.rb index 6e669c8eef1..d4e784361d2 100644 --- a/lib/active_merchant/billing/gateways/banwire.rb +++ b/lib/active_merchant/billing/gateways/banwire.rb @@ -89,11 +89,13 @@ def commit(money, parameters) response = json_error(raw_response) end - Response.new(success?(response), + Response.new( + success?(response), response['message'], response, test: test?, - authorization: response['code_auth']) + authorization: response['code_auth'] + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/beanstream.rb b/lib/active_merchant/billing/gateways/beanstream.rb index 68168211868..96509d7388b 100644 --- a/lib/active_merchant/billing/gateways/beanstream.rb +++ b/lib/active_merchant/billing/gateways/beanstream.rb @@ -76,6 +76,7 @@ def authorize(money, source, options = {}) add_transaction_type(post, :authorization) add_customer_ip(post, options) add_recurring_payment(post, options) + add_three_ds(post, options) commit(post) end @@ -88,6 +89,7 @@ def purchase(money, source, options = {}) add_transaction_type(post, purchase_action(source)) add_customer_ip(post, options) add_recurring_payment(post, options) + add_three_ds(post, options) commit(post) end @@ -215,6 +217,22 @@ def scrub(transcript) def build_response(*args) Response.new(*args) end + + def add_three_ds(post, options) + return unless three_d_secure = options[:three_d_secure] + + post[:SecureXID] = (three_d_secure[:ds_transaction_id] || three_d_secure[:xid]) if three_d_secure.slice(:ds_transaction_id, :xid).values.any? + post[:SecureECI] = formatted_three_ds_eci(three_d_secure[:eci]) if three_d_secure[:eci].present? + post[:SecureCAVV] = three_d_secure[:cavv] if three_d_secure[:cavv].present? + end + + def formatted_three_ds_eci(val) + case val + when '05', '02' then 5 + when '06', '01' then 6 + else val.to_i + end + end end end end diff --git a/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb b/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb index b794899c579..2239c87a75d 100644 --- a/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb +++ b/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb @@ -413,11 +413,15 @@ def recurring_commit(params) def post(data, use_profile_api = nil) response = parse(ssl_post((use_profile_api ? SECURE_PROFILE_URL : self.live_url), data)) response[:customer_vault_id] = response[:customerCode] if response[:customerCode] - build_response(success?(response), message_from(response), response, + build_response( + success?(response), + message_from(response), + response, test: test? || response[:authCode] == 'TEST', authorization: authorization_from(response), cvv_result: CVD_CODES[response[:cvdId]], - avs_result: { code: AVS_CODES.include?(response[:avsId]) ? AVS_CODES[response[:avsId]] : response[:avsId] }) + avs_result: { code: AVS_CODES.include?(response[:avsId]) ? AVS_CODES[response[:avsId]] : response[:avsId] } + ) end def recurring_post(data) diff --git a/lib/active_merchant/billing/gateways/blue_pay.rb b/lib/active_merchant/billing/gateways/blue_pay.rb index 60b6fc863a7..b1e60343f17 100644 --- a/lib/active_merchant/billing/gateways/blue_pay.rb +++ b/lib/active_merchant/billing/gateways/blue_pay.rb @@ -344,9 +344,13 @@ def parse_recurring(response_fields, opts = {}) # expected status? success = parsed[:status] != 'error' message = parsed[:status] - Response.new(success, message, parsed, + Response.new( + success, + message, + parsed, test: test?, - authorization: parsed[:rebill_id]) + authorization: parsed[:rebill_id] + ) end def parse(body) @@ -364,11 +368,15 @@ def parse(body) # normalize message message = message_from(parsed) success = parsed[:response_code] == '1' - Response.new(success, message, parsed, + Response.new( + success, + message, + parsed, test: test?, authorization: (parsed[:rebid] && parsed[:rebid] != '' ? parsed[:rebid] : parsed[:transaction_id]), avs_result: { code: parsed[:avs_result_code] }, - cvv_result: parsed[:card_code]) + cvv_result: parsed[:card_code] + ) end def message_from(parsed) diff --git a/lib/active_merchant/billing/gateways/blue_snap.rb b/lib/active_merchant/billing/gateways/blue_snap.rb index e8b7e5ef1b3..5abac55c16b 100644 --- a/lib/active_merchant/billing/gateways/blue_snap.rb +++ b/lib/active_merchant/billing/gateways/blue_snap.rb @@ -68,6 +68,8 @@ class BlueSnapGateway < Gateway 'business_savings' => 'CORPORATE_SAVINGS' } + SHOPPER_INITIATOR = %w(CUSTOMER CARDHOLDER) + STATE_CODE_COUNTRIES = %w(US CA) def initialize(options = {}) @@ -179,6 +181,7 @@ def add_auth_purchase(doc, money, payment_method, options) doc.send('store-card', options[:store_card] || false) add_amount(doc, money, options) add_fraud_info(doc, payment_method, options) + add_stored_credentials(doc, options) if payment_method.is_a?(String) doc.send('vaulted-shopper-id', payment_method) @@ -190,6 +193,19 @@ def add_auth_purchase(doc, money, payment_method, options) end end + def add_stored_credentials(doc, options) + return unless stored_credential = options[:stored_credential] + + initiator = stored_credential[:initiator]&.upcase + initiator = 'SHOPPER' if SHOPPER_INITIATOR.include?(initiator) + doc.send('transaction-initiator', initiator) if stored_credential[:initiator] + if stored_credential[:network_transaction_id] + doc.send('network-transaction-info') do + doc.send('original-network-transaction-id', stored_credential[:network_transaction_id]) + end + end + end + def add_amount(doc, money, options) currency = options[:currency] || currency(money) doc.amount(localized_amount(money, currency)) @@ -244,6 +260,7 @@ def add_metadata(doc, options) def add_order(doc, options) doc.send('merchant-transaction-id', truncate(options[:order_id], 50)) if options[:order_id] doc.send('soft-descriptor', options[:soft_descriptor]) if options[:soft_descriptor] + doc.send('descriptor-phone-number', options[:descriptor_phone_number]) if options[:descriptor_phone_number] add_metadata(doc, options) add_3ds(doc, options[:three_d_secure]) if options[:three_d_secure] add_level_3_data(doc, options) @@ -350,6 +367,7 @@ def add_shipping_contact_info(doc, payment_method, options) def add_alt_transaction_purchase(doc, money, payment_method_details, options) doc.send('merchant-transaction-id', truncate(options[:order_id], 50)) if options[:order_id] doc.send('soft-descriptor', options[:soft_descriptor]) if options[:soft_descriptor] + doc.send('descriptor-phone-number', options[:descriptor_phone_number]) if options[:descriptor_phone_number] add_amount(doc, money, options) vaulted_shopper_id = payment_method_details.vaulted_shopper_id @@ -358,6 +376,7 @@ def add_alt_transaction_purchase(doc, money, payment_method_details, options) add_echeck_transaction(doc, payment_method_details.payment_method, options, vaulted_shopper_id.present?) if payment_method_details.check? add_fraud_info(doc, payment_method_details.payment_method, options) + add_stored_credentials(doc, options) add_metadata(doc, options) end @@ -512,7 +531,9 @@ def fraud_codes_from(response) end def authorization_from(action, parsed_response, payment_method_details) - action == :store ? vaulted_shopper_id(parsed_response, payment_method_details) : parsed_response['transaction-id'] + return vaulted_shopper_id(parsed_response, payment_method_details) if action == :store + + parsed_response['refund-transaction-id'] || parsed_response['transaction-id'] end def vaulted_shopper_id(parsed_response, payment_method_details) diff --git a/lib/active_merchant/billing/gateways/bogus.rb b/lib/active_merchant/billing/gateways/bogus.rb index 9aed8028586..30b8be9838a 100644 --- a/lib/active_merchant/billing/gateways/bogus.rb +++ b/lib/active_merchant/billing/gateways/bogus.rb @@ -90,6 +90,10 @@ def void(reference, options = {}) end end + def verify(credit_card, options = {}) + authorize(0, credit_card, options) + end + def store(paysource, options = {}) case normalize(paysource) when /1$/ diff --git a/lib/active_merchant/billing/gateways/borgun.rb b/lib/active_merchant/billing/gateways/borgun.rb index 80ac3ca4f57..778c6bc64eb 100644 --- a/lib/active_merchant/billing/gateways/borgun.rb +++ b/lib/active_merchant/billing/gateways/borgun.rb @@ -23,18 +23,34 @@ def initialize(options = {}) def purchase(money, payment, options = {}) post = {} - post[:TransType] = '1' + action = '' + if options[:apply_3d_secure] == '1' + add_3ds_preauth_fields(post, options) + action = '3ds_preauth' + else + post[:TransType] = '1' + add_3ds_fields(post, options) + action = 'sale' + end add_invoice(post, money, options) add_payment_method(post, payment) - commit('sale', post) + commit(action, post, options) end def authorize(money, payment, options = {}) post = {} - post[:TransType] = '5' + action = '' + if options[:apply_3d_secure] == '1' + add_3ds_preauth_fields(post, options) + action = '3ds_preauth' + else + post[:TransType] = '5' + add_3ds_fields(post, options) + action = 'authonly' + end add_invoice(post, money, options) add_payment_method(post, payment) - commit('authonly', post, options) + commit(action, post, options) end def capture(money, authorization, options = {}) @@ -80,10 +96,28 @@ def scrub(transcript) CURRENCY_CODES['ISK'] = '352' CURRENCY_CODES['EUR'] = '978' CURRENCY_CODES['USD'] = '840' + CURRENCY_CODES['GBP'] = '826' + + def add_3ds_fields(post, options) + post[:ThreeDSMessageId] = options[:three_ds_message_id] if options[:three_ds_message_id] + post[:ThreeDS_PARes] = options[:three_ds_pares] if options[:three_ds_pares] + post[:ThreeDS_CRes] = options[:three_ds_cres] if options[:three_ds_cres] + end + + def add_3ds_preauth_fields(post, options) + post[:SaleDescription] = options[:sale_description] || '' + post[:MerchantReturnURL] = options[:redirect_url] if options[:redirect_url] + end def add_invoice(post, money, options) post[:TrAmount] = amount(money) post[:TrCurrency] = CURRENCY_CODES[options[:currency] || currency(money)] + # The ISK currency must have a currency exponent of 2 on the 3DS request but not on the auth request + if post[:TrCurrency] == '352' && options[:apply_3d_secure] != '1' + post[:TrCurrencyExponent] = 0 + else + post[:TrCurrencyExponent] = 2 + end post[:TerminalID] = options[:terminal_id] || '1' end @@ -103,11 +137,11 @@ def add_reference(post, authorization) post[:AuthCode] = authcode end - def parse(xml) + def parse(xml, options = nil) response = {} doc = Nokogiri::XML(CGI.unescapeHTML(xml)) - body = doc.xpath('//getAuthorizationReply') + body = options[:apply_3d_secure] == '1' ? doc.xpath('//get3DSAuthenticationReply') : doc.xpath('//getAuthorizationReply') body = doc.xpath('//cancelAuthorizationReply') if body.length == 0 body.children.each do |node| if node.text? @@ -121,7 +155,6 @@ def parse(xml) end end end - response end @@ -132,32 +165,32 @@ def commit(action, post, options = {}) request = build_request(action, post, options) raw = ssl_post(url(action), request, headers) - pairs = parse(raw) + pairs = parse(raw, options) success = success_from(pairs) Response.new( success, message_from(success, pairs), pairs, - authorization: authorization_from(pairs), + authorization: authorization_from(pairs, options), test: test? ) end def success_from(response) - (response[:actioncode] == '000') + (response[:actioncode] == '000') || (response[:status_resultcode] == '0') end def message_from(succeeded, response) if succeeded 'Succeeded' else - response[:message] || "Error with ActionCode=#{response[:actioncode]}" + response[:message] || response[:status_errormessage] || "Error with ActionCode=#{response[:actioncode]}" end end - def authorization_from(response) - [ + def authorization_from(response, options) + authorization = [ response[:dateandtime], response[:batch], response[:transaction], @@ -167,6 +200,8 @@ def authorization_from(response) response[:tramount], response[:trcurrency] ].join('|') + + authorization == '|||||||' ? nil : authorization end def split_authorization(authorization) @@ -182,16 +217,17 @@ def headers def build_request(action, post, options = {}) mode = action == 'void' ? 'cancel' : 'get' + transaction_type = action == '3ds_preauth' ? '3DSAuthentication' : 'Authorization' xml = Builder::XmlMarkup.new indent: 18 xml.instruct!(:xml, version: '1.0', encoding: 'utf-8') - xml.tag!("#{mode}Authorization") do + xml.tag!("#{mode}#{transaction_type}") do post.each do |field, value| xml.tag!(field, value) end build_airline_xml(xml, options[:passenger_itinerary_data]) if options[:passenger_itinerary_data] end inner = CGI.escapeHTML(xml.target!) - envelope(mode).sub(/{{ :body }}/, inner) + envelope(mode, action).sub(/{{ :body }}/, inner) end def build_airline_xml(xml, airline_data) @@ -204,16 +240,23 @@ def build_airline_xml(xml, airline_data) end end - def envelope(mode) + def envelope(mode, action) + if action == '3ds_preauth' + transaction_action = "#{mode}3DSAuthentication" + request_action = "#{mode}Auth3DSReqXml" + else + transaction_action = "#{mode}AuthorizationInput" + request_action = "#{mode}AuthReqXml" + end <<-XML - - <#{mode}AuthReqXml> + + <#{request_action}> {{ :body }} - - + + XML diff --git a/lib/active_merchant/billing/gateways/braintree_blue.rb b/lib/active_merchant/billing/gateways/braintree_blue.rb index f5edfb6e8d4..91a27f00ec1 100644 --- a/lib/active_merchant/billing/gateways/braintree_blue.rb +++ b/lib/active_merchant/billing/gateways/braintree_blue.rb @@ -75,6 +75,12 @@ def initialize(options = {}) @braintree_gateway = Braintree::Gateway.new(@configuration) end + def setup_purchase + commit do + Response.new(true, 'Client token created', { client_token: @braintree_gateway.client_token.generate }) + end + end + def authorize(money, credit_card_or_vault_id, options = {}) return Response.new(false, DIRECT_BANK_ERROR) if credit_card_or_vault_id.is_a? Check @@ -82,9 +88,16 @@ def authorize(money, credit_card_or_vault_id, options = {}) end def capture(money, authorization, options = {}) - commit do - result = @braintree_gateway.transaction.submit_for_settlement(authorization, localized_amount(money, options[:currency] || default_currency).to_s) - response_from_result(result) + if options[:partial_capture] == true + commit do + result = @braintree_gateway.transaction.submit_for_partial_settlement(authorization, localized_amount(money, options[:currency] || default_currency).to_s) + response_from_result(result) + end + else + commit do + result = @braintree_gateway.transaction.submit_for_settlement(authorization, localized_amount(money, options[:currency] || default_currency).to_s) + response_from_result(result) + end end end @@ -93,6 +106,8 @@ def purchase(money, credit_card_or_vault_id, options = {}) end def credit(money, credit_card_or_vault_id, options = {}) + return Response.new(false, DIRECT_BANK_ERROR) if credit_card_or_vault_id.is_a? Check + create_transaction(:credit, money, credit_card_or_vault_id, options) end @@ -180,16 +195,20 @@ def update(vault_id, creditcard, options = {}) } }, options)[:credit_card] - result = @braintree_gateway.customer.update(vault_id, + result = @braintree_gateway.customer.update( + vault_id, first_name: creditcard.first_name, last_name: creditcard.last_name, email: scrub_email(options[:email]), - phone: options[:phone] || (options[:billing_address][:phone] if options[:billing_address] && - options[:billing_address][:phone]), - credit_card: credit_card_params) - Response.new(result.success?, message_from_result(result), + phone: phone_from(options), + credit_card: credit_card_params + ) + Response.new( + result.success?, + message_from_result(result), braintree_customer: (customer_hash(@braintree_gateway.customer.find(vault_id), :include_credit_cards) if result.success?), - customer_vault_id: (result.customer.id if result.success?)) + customer_vault_id: (result.customer.id if result.success?) + ) end end @@ -254,19 +273,21 @@ def add_customer_with_credit_card(creditcard, options) first_name: creditcard.first_name, last_name: creditcard.last_name, email: scrub_email(options[:email]), - phone: options[:phone] || (options[:billing_address][:phone] if options[:billing_address] && - options[:billing_address][:phone]), + phone: phone_from(options), id: options[:customer], device_data: options[:device_data] }.merge credit_card_params result = @braintree_gateway.customer.create(merge_credit_card_options(parameters, options)) - Response.new(result.success?, message_from_result(result), + Response.new( + result.success?, + message_from_result(result), { braintree_customer: (customer_hash(result.customer, :include_credit_cards) if result.success?), customer_vault_id: (result.customer.id if result.success?), credit_card_token: (result.customer.credit_cards[0].token if result.success?) }, - authorization: (result.customer.id if result.success?)) + authorization: (result.customer.id if result.success?) + ) end end @@ -335,6 +356,10 @@ def merge_credit_card_options(parameters, options) parameters end + def phone_from(options) + options[:phone] || options.dig(:billing_address, :phone) || options.dig(:billing_address, :phone_number) + end + def map_address(address) mapped = { street_address: address[:address1], @@ -356,8 +381,8 @@ def map_address(address) def commit(&block) yield - rescue Braintree::BraintreeError => ex - Response.new(false, ex.class.to_s) + rescue Braintree::BraintreeError => e + Response.new(false, e.class.to_s) end def message_from_result(result) @@ -467,16 +492,28 @@ def response_code_from_result(result) end end + def additional_processor_response_from_result(result) + result.transaction&.additional_processor_response + end + def create_transaction(transaction_type, money, credit_card_or_vault_id, options) transaction_params = create_transaction_parameters(money, credit_card_or_vault_id, options) commit do result = @braintree_gateway.transaction.send(transaction_type, transaction_params) + make_default_payment_method_token(result) if options.dig(:paypal, :paypal_flow_type) == 'checkout_with_vault' && result.success? response = Response.new(result.success?, message_from_transaction_result(result), response_params(result), response_options(result)) response.cvv_result['message'] = '' response end end + def make_default_payment_method_token(result) + @braintree_gateway.customer.update( + result.transaction.customer_details.id, + default_payment_method_token: result.transaction.paypal_details.implicitly_vaulted_payment_method_token + ) + end + def extract_refund_args(args) options = args.extract_options! @@ -516,7 +553,10 @@ def customer_hash(customer, include_credit_cards = false) end def transaction_hash(result) - return { 'processor_response_code' => response_code_from_result(result) } unless result.success? + unless result.success? + return { 'processor_response_code' => response_code_from_result(result), + 'additional_processor_response' => additional_processor_response_from_result(result) } + end transaction = result.transaction if transaction.vault_customer @@ -561,7 +601,10 @@ def transaction_hash(result) 'bin' => transaction.credit_card_details.bin, 'last_4' => transaction.credit_card_details.last_4, 'card_type' => transaction.credit_card_details.card_type, - 'token' => transaction.credit_card_details.token + 'token' => transaction.credit_card_details.token, + 'debit' => transaction.credit_card_details.debit, + 'prepaid' => transaction.credit_card_details.prepaid, + 'issuing_bank' => transaction.credit_card_details.issuing_bank } if transaction.risk_data @@ -575,20 +618,30 @@ def transaction_hash(result) risk_data = nil end + if transaction.payment_receipt + payment_receipt = { + 'global_id' => transaction.payment_receipt.global_id + } + else + payment_receipt = nil + end + { - 'order_id' => transaction.order_id, - 'amount' => transaction.amount.to_s, - 'status' => transaction.status, - 'credit_card_details' => credit_card_details, - 'customer_details' => customer_details, - 'billing_details' => billing_details, - 'shipping_details' => shipping_details, - 'vault_customer' => vault_customer, - 'merchant_account_id' => transaction.merchant_account_id, - 'risk_data' => risk_data, - 'network_transaction_id' => transaction.network_transaction_id || nil, - 'processor_response_code' => response_code_from_result(result), - 'recurring' => transaction.recurring + 'order_id' => transaction.order_id, + 'amount' => transaction.amount.to_s, + 'status' => transaction.status, + 'credit_card_details' => credit_card_details, + 'customer_details' => customer_details, + 'billing_details' => billing_details, + 'shipping_details' => shipping_details, + 'vault_customer' => vault_customer, + 'merchant_account_id' => transaction.merchant_account_id, + 'risk_data' => risk_data, + 'network_transaction_id' => transaction.network_transaction_id || nil, + 'processor_response_code' => response_code_from_result(result), + 'processor_authorization_code' => transaction.processor_authorization_code, + 'recurring' => transaction.recurring, + 'payment_receipt' => payment_receipt } end @@ -599,8 +652,7 @@ def create_transaction_parameters(money, credit_card_or_vault_id, options) customer: { id: options[:store] == true ? '' : options[:store], email: scrub_email(options[:email]), - phone: options[:phone] || (options[:billing_address][:phone] if options[:billing_address] && - options[:billing_address][:phone]) + phone: phone_from(options) }, options: { store_in_vault: options[:store] ? true : false, @@ -616,6 +668,7 @@ def create_transaction_parameters(money, credit_card_or_vault_id, options) add_account_type(parameters, options) if options[:account_type] add_skip_options(parameters, options) add_merchant_account_id(parameters, options) + add_profile_id(parameters, options) add_payment_method(parameters, credit_card_or_vault_id, options) add_stored_credential_data(parameters, credit_card_or_vault_id, options) @@ -623,6 +676,7 @@ def create_transaction_parameters(money, credit_card_or_vault_id, options) add_descriptor(parameters, options) add_risk_data(parameters, options) + add_paypal_options(parameters, options) add_travel_data(parameters, options) if options[:travel_data] add_lodging_data(parameters, options) if options[:lodging_data] add_channel(parameters, options) @@ -633,6 +687,8 @@ def create_transaction_parameters(money, credit_card_or_vault_id, options) add_3ds_info(parameters, options[:three_d_secure]) + parameters[:sca_exemption] = options[:three_ds_exemption_type] if options[:three_ds_exemption_type] + if options[:payment_method_nonce].is_a?(String) parameters.delete(:customer) parameters[:payment_method_nonce] = options[:payment_method_nonce] @@ -658,6 +714,13 @@ def add_merchant_account_id(parameters, options) parameters[:merchant_account_id] = merchant_account_id end + def add_profile_id(parameters, options) + return unless profile_id = options[:venmo_profile_id] + + parameters[:options][:venmo] = {} + parameters[:options][:venmo][:profile_id] = profile_id + end + def add_transaction_source(parameters, options) parameters[:transaction_source] = options[:transaction_source] if options[:transaction_source] parameters[:transaction_source] = 'recurring' if options[:recurring] @@ -692,6 +755,15 @@ def add_risk_data(parameters, options) } end + def add_paypal_options(parameters, options) + return unless options[:paypal_custom_field] || options[:paypal_description] + + parameters[:options][:paypal] = { + custom_field: options[:paypal_custom_field], + description: options[:paypal_description] + } + end + def add_level_2_data(parameters, options) parameters[:tax_amount] = options[:tax_amount] if options[:tax_amount] parameters[:tax_exempt] = options[:tax_exempt] if options[:tax_exempt] @@ -779,54 +851,86 @@ def add_stored_credential_data(parameters, credit_card_or_vault_id, options) def add_payment_method(parameters, credit_card_or_vault_id, options) if credit_card_or_vault_id.is_a?(String) || credit_card_or_vault_id.is_a?(Integer) - if options[:payment_method_token] - parameters[:payment_method_token] = credit_card_or_vault_id - options.delete(:billing_address) - elsif options[:payment_method_nonce] - parameters[:payment_method_nonce] = credit_card_or_vault_id - else - parameters[:customer_id] = credit_card_or_vault_id - end + add_third_party_token(parameters, credit_card_or_vault_id, options) else parameters[:customer].merge!( first_name: credit_card_or_vault_id.first_name, last_name: credit_card_or_vault_id.last_name ) if credit_card_or_vault_id.is_a?(NetworkTokenizationCreditCard) - if credit_card_or_vault_id.source == :apple_pay - parameters[:apple_pay_card] = { - number: credit_card_or_vault_id.number, - expiration_month: credit_card_or_vault_id.month.to_s.rjust(2, '0'), - expiration_year: credit_card_or_vault_id.year.to_s, - cardholder_name: credit_card_or_vault_id.name, - cryptogram: credit_card_or_vault_id.payment_cryptogram, - eci_indicator: credit_card_or_vault_id.eci - } - elsif credit_card_or_vault_id.source == :android_pay || credit_card_or_vault_id.source == :google_pay - Braintree::Version::Major < 3 ? pay_card = :android_pay_card : pay_card = :google_pay_card - parameters[pay_card] = { - number: credit_card_or_vault_id.number, - cryptogram: credit_card_or_vault_id.payment_cryptogram, - expiration_month: credit_card_or_vault_id.month.to_s.rjust(2, '0'), - expiration_year: credit_card_or_vault_id.year.to_s, - google_transaction_id: credit_card_or_vault_id.transaction_id, - source_card_type: credit_card_or_vault_id.brand, - source_card_last_four: credit_card_or_vault_id.last_digits, - eci_indicator: credit_card_or_vault_id.eci - } + case credit_card_or_vault_id.source + when :apple_pay + add_apple_pay(parameters, credit_card_or_vault_id) + when :google_pay + add_google_pay(parameters, credit_card_or_vault_id) + else + add_network_tokenization_card(parameters, credit_card_or_vault_id) end else - parameters[:credit_card] = { - number: credit_card_or_vault_id.number, - cvv: credit_card_or_vault_id.verification_value, - expiration_month: credit_card_or_vault_id.month.to_s.rjust(2, '0'), - expiration_year: credit_card_or_vault_id.year.to_s, - cardholder_name: credit_card_or_vault_id.name - } + add_credit_card(parameters, credit_card_or_vault_id) end end end + def add_third_party_token(parameters, payment_method, options) + if options[:payment_method_token] + parameters[:payment_method_token] = payment_method + options.delete(:billing_address) + elsif options[:payment_method_nonce] + parameters[:payment_method_nonce] = payment_method + else + parameters[:customer_id] = payment_method + end + end + + def add_credit_card(parameters, payment_method) + parameters[:credit_card] = { + number: payment_method.number, + cvv: payment_method.verification_value, + expiration_month: payment_method.month.to_s.rjust(2, '0'), + expiration_year: payment_method.year.to_s, + cardholder_name: payment_method.name + } + end + + def add_apple_pay(parameters, payment_method) + parameters[:apple_pay_card] = { + number: payment_method.number, + expiration_month: payment_method.month.to_s.rjust(2, '0'), + expiration_year: payment_method.year.to_s, + cardholder_name: payment_method.name, + cryptogram: payment_method.payment_cryptogram, + eci_indicator: payment_method.eci + } + end + + def add_google_pay(parameters, payment_method) + Braintree::Version::Major < 3 ? pay_card = :android_pay_card : pay_card = :google_pay_card + parameters[pay_card] = { + number: payment_method.number, + cryptogram: payment_method.payment_cryptogram, + expiration_month: payment_method.month.to_s.rjust(2, '0'), + expiration_year: payment_method.year.to_s, + google_transaction_id: payment_method.transaction_id, + source_card_type: payment_method.brand, + source_card_last_four: payment_method.last_digits, + eci_indicator: payment_method.eci + } + end + + def add_network_tokenization_card(parameters, payment_method) + parameters[:credit_card] = { + number: payment_method.number, + expiration_month: payment_method.month.to_s.rjust(2, '0'), + expiration_year: payment_method.year.to_s, + cardholder_name: payment_method.name, + network_tokenization_attributes: { + cryptogram: payment_method.payment_cryptogram, + ecommerce_indicator: payment_method.eci + } + } + end + def bank_account_errors(payment_method, options) if payment_method.validate.present? payment_method.validate @@ -853,13 +957,16 @@ def add_bank_account_to_customer(payment_method, options) message = message_from_result(result) message = not_verified_reason(result.payment_method) unless verified - Response.new(verified, message, + Response.new( + verified, + message, { customer_vault_id: options[:customer], bank_account_token: result.payment_method&.token, verified: verified }, - authorization: result.payment_method&.token) + authorization: result.payment_method&.token + ) end def not_verified_reason(bank_account) @@ -885,7 +992,7 @@ def create_customer_from_bank_account(payment_method, options) first_name: payment_method.first_name, last_name: payment_method.last_name, email: scrub_email(options[:email]), - phone: options[:phone] || options.dig(:billing_address, :phone), + phone: phone_from(options), device_data: options[:device_data] }.compact diff --git a/lib/active_merchant/billing/gateways/card_connect.rb b/lib/active_merchant/billing/gateways/card_connect.rb index d760d2179b6..6a803aeb322 100644 --- a/lib/active_merchant/billing/gateways/card_connect.rb +++ b/lib/active_merchant/billing/gateways/card_connect.rb @@ -1,8 +1,8 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: class CardConnectGateway < Gateway - self.test_url = 'https://fts.cardconnect.com:6443/cardconnect/rest/' - self.live_url = 'https://fts.cardconnect.com:8443/cardconnect/rest/' + self.test_url = 'https://fts-uat.cardconnect.com/cardconnect/rest/' + self.live_url = 'https://fts.cardconnect.com/cardconnect/rest/' self.supported_countries = ['US'] self.default_currency = 'USD' @@ -61,6 +61,8 @@ class CardConnectGateway < Gateway '60' => STANDARD_ERROR_CODE[:pickup_card] } + SCHEDULED_PAYMENT_TYPES = %w(recurring installment) + def initialize(options = {}) requires!(options, :merchant_id, :username, :password) require_valid_domain!(options, :domain) @@ -87,8 +89,9 @@ def purchase(money, payment, options = {}) add_currency(post, money, options) add_address(post, options) add_customer_data(post, options) - add_3DS(post, options) + add_three_ds_mpi_data(post, options) add_additional_data(post, options) + add_stored_credential(post, options) post[:capture] = 'Y' commit('auth', post) end @@ -102,8 +105,9 @@ def authorize(money, payment, options = {}) add_payment(post, payment) add_address(post, options) add_customer_data(post, options) - add_3DS(post, options) + add_three_ds_mpi_data(post, options) add_additional_data(post, options) + add_stored_credential(post, options) commit('auth', post) end @@ -142,9 +146,12 @@ def store(payment, options = {}) def unstore(authorization, options = {}) account_id, profile_id = authorization.split('|') - commit('profile', {}, + commit( + 'profile', + {}, verb: :delete, - path: "/#{profile_id}/#{account_id}/#{@options[:merchant_id]}") + path: "/#{profile_id}/#{account_id}/#{@options[:merchant_id]}" + ) end def supports_scrubbing? @@ -188,7 +195,11 @@ def add_currency(post, money, options) def add_invoice(post, options) post[:orderid] = options[:order_id] - post[:ecomind] = (options[:recurring] ? 'R' : 'E') + post[:ecomind] = if options[:ecomind] + options[:ecomind].capitalize + else + (options[:recurring] ? 'R' : 'E') + end end def add_payment(post, payment) @@ -241,10 +252,20 @@ def add_additional_data(post, options) post[:userfields] = options[:user_fields] if options[:user_fields] end - def add_3DS(post, options) - post[:secureflag] = options[:secure_flag] if options[:secure_flag] - post[:securevalue] = options[:secure_value] if options[:secure_value] - post[:securexid] = options[:secure_xid] if options[:secure_xid] + def add_three_ds_mpi_data(post, options) + return unless three_d_secure = options[:three_d_secure] + + post[:secureflag] = three_d_secure[:eci] + post[:securevalue] = three_d_secure[:cavv] + post[:securedstid] = three_d_secure[:ds_transaction_id] + end + + def add_stored_credential(post, options) + return unless stored_credential = options[:stored_credential] + + post[:cof] = stored_credential[:initiator] == 'merchant' ? 'M' : 'C' + post[:cofscheduled] = SCHEDULED_PAYMENT_TYPES.include?(stored_credential[:reason_type]) ? 'Y' : 'N' + post[:cofpermission] = stored_credential[:initial_transaction] ? 'Y' : 'N' end def headers diff --git a/lib/active_merchant/billing/gateways/card_stream.rb b/lib/active_merchant/billing/gateways/card_stream.rb index f28fd24901b..76c8f318519 100644 --- a/lib/active_merchant/billing/gateways/card_stream.rb +++ b/lib/active_merchant/billing/gateways/card_stream.rb @@ -222,6 +222,7 @@ def add_auth_purchase(post, pair_value, money, credit_card_or_reference, options add_customer_data(post, options) add_remote_address(post, options) add_country_code(post, options) + add_threeds_fields(post, options) end def add_amount(post, money, options) @@ -283,6 +284,20 @@ def add_threeds_required(post, options) add_pair(post, :threeDSRequired, options[:threeds_required] || @threeds_required ? 'Y' : 'N') end + def add_threeds_fields(post, options) + return unless three_d_secure = options[:three_d_secure] + + add_pair(post, :threeDSEnrolled, formatted_enrollment(three_d_secure[:enrolled])) + if three_d_secure[:enrolled] == 'true' + add_pair(post, :threeDSAuthenticated, three_d_secure[:authentication_response_status]) + if three_d_secure[:authentication_response_status] == 'Y' + post[:threeDSECI] = three_d_secure[:eci] + post[:threeDSCAVV] = three_d_secure[:cavv] + post[:threeDSXID] = three_d_secure[:xid] || three_d_secure[:ds_transaction_id] + end + end + end + def add_remote_address(post, options = {}) add_pair(post, :remoteAddress, options[:ip] || '1.1.1.1') end @@ -366,6 +381,14 @@ def post_data(action, parameters = {}) def add_pair(post, key, value, options = {}) post[key] = value if !value.blank? || options[:required] end + + def formatted_enrollment(val) + case val + when 'Y', 'N', 'U' then val + when true, 'true' then 'Y' + when false, 'false' then 'N' + end + end end end end diff --git a/lib/active_merchant/billing/gateways/checkout_v2.rb b/lib/active_merchant/billing/gateways/checkout_v2.rb index a607ad00290..d0dddaff429 100644 --- a/lib/active_merchant/billing/gateways/checkout_v2.rb +++ b/lib/active_merchant/billing/gateways/checkout_v2.rb @@ -9,12 +9,24 @@ class CheckoutV2Gateway < Gateway self.supported_countries = %w[AD AE AR AT AU BE BG BH BR CH CL CN CO CY CZ DE DK EE EG ES FI FR GB GR HK HR HU IE IS IT JO JP KW LI LT LU LV MC MT MX MY NL NO NZ OM PE PL PT QA RO SA SE SG SI SK SM TR US] self.default_currency = 'USD' self.money_format = :cents - self.supported_cardtypes = %i[visa master american_express diners_club maestro discover jcb] + self.supported_cardtypes = %i[visa master american_express diners_club maestro discover jcb mada bp_plus] self.currencies_without_fractions = %w(BIF DJF GNF ISK KMF XAF CLF XPF JPY PYG RWF KRW VUV VND XOF) self.currencies_with_three_decimal_places = %w(BHD LYD JOD KWD OMR TND) + LIVE_ACCESS_TOKEN_URL = 'https://access.checkout.com/connect/token' + TEST_ACCESS_TOKEN_URL = 'https://access.sandbox.checkout.com/connect/token' + def initialize(options = {}) - requires!(options, :secret_key) + @options = options + @access_token = nil + + if options.has_key?(:secret_key) + requires!(options, :secret_key) + else + requires!(options, :client_id, :client_secret) + @access_token = setup_access_token + end + super end @@ -22,31 +34,45 @@ def purchase(amount, payment_method, options = {}) post = {} build_auth_or_purchase(post, amount, payment_method, options) - commit(:purchase, post) + commit(:purchase, post, options) end def authorize(amount, payment_method, options = {}) post = {} post[:capture] = false build_auth_or_purchase(post, amount, payment_method, options) - - commit(:authorize, post) + options[:incremental_authorization] ? commit(:incremental_authorize, post, options, options[:incremental_authorization]) : commit(:authorize, post, options) end def capture(amount, authorization, options = {}) post = {} + post[:capture_type] = options[:capture_type] || 'Final' add_invoice(post, amount, options) add_customer_data(post, options) + add_shipping_address(post, options) add_metadata(post, options) - commit(:capture, post, authorization) + commit(:capture, post, options, authorization) + end + + def credit(amount, payment, options = {}) + post = {} + add_processing_channel(post, options) + add_invoice(post, amount, options) + add_payment_method(post, payment, options, :destination) + add_source(post, options) + add_instruction_data(post, options) + add_payout_sender_data(post, options) + add_payout_destination_data(post, options) + + commit(:credit, post, options) end def void(authorization, _options = {}) post = {} add_metadata(post, options) - commit(:void, post, authorization) + commit(:void, post, options, authorization) end def refund(amount, authorization, options = {}) @@ -55,7 +81,7 @@ def refund(amount, authorization, options = {}) add_customer_data(post, options) add_metadata(post, options) - commit(:refund, post, authorization) + commit(:refund, post, options, authorization) end def verify(credit_card, options = {}) @@ -63,7 +89,7 @@ def verify(credit_card, options = {}) end def verify_payment(authorization, option = {}) - commit(:verify_payment, authorization) + commit(:verify_payment, nil, options, authorization, :get) end def supports_scrubbing? @@ -74,19 +100,52 @@ def scrub(transcript) transcript. gsub(/(Authorization: )[^\\]*/i, '\1[FILTERED]'). gsub(/("number\\":\\")\d+/, '\1[FILTERED]'). - gsub(/("cvv\\":\\")\d+/, '\1[FILTERED]') + gsub(/("cvv\\":\\")\d+/, '\1[FILTERED]'). + gsub(/("cryptogram\\":\\")\w+/, '\1[FILTERED]'). + gsub(/(source\\":\{.*\\"token\\":\\")\d+/, '\1[FILTERED]'). + gsub(/("token\\":\\")\w+/, '\1[FILTERED]') + end + + def store(payment_method, options = {}) + post = {} + MultiResponse.run do |r| + if payment_method.is_a?(NetworkTokenizationCreditCard) + r.process { verify(payment_method, options) } + break r unless r.success? + + r.params['source']['customer'] = r.params['customer'] + r.process { response(:store, true, r.params['source']) } + else + r.process { tokenize(payment_method, options) } + break r unless r.success? + + token = r.params['token'] + add_payment_method(post, token, options) + post.merge!(post.delete(:source)) + add_customer_data(post, options) + add_shipping_address(post, options) + r.process { commit(:store, post, options) } + end + end + end + + def unstore(id, options = {}) + commit(:unstore, nil, options, id, :delete) end private def build_auth_or_purchase(post, amount, payment_method, options) add_invoice(post, amount, options) + add_authorization_type(post, options) add_payment_method(post, payment_method, options) add_customer_data(post, options) + add_extra_customer_data(post, payment_method, options) + add_shipping_address(post, options) add_stored_credential_options(post, options) add_transaction_data(post, options) add_3ds(post, options) - add_metadata(post, options) + add_metadata(post, options, payment_method) add_processing_channel(post, options) add_marketplace_data(post, options) end @@ -104,33 +163,77 @@ def add_invoice(post, money, options) post[:metadata][:udf5] = application_id || 'ActiveMerchant' end - def add_metadata(post, options) + def add_authorization_type(post, options) + post[:authorization_type] = options[:authorization_type] if options[:authorization_type] + end + + def add_metadata(post, options, payment_method = nil) post[:metadata] = {} unless post[:metadata] post[:metadata].merge!(options[:metadata]) if options[:metadata] + post[:metadata][:udf1] = 'mada' if payment_method.try(:brand) == 'mada' end - def add_payment_method(post, payment_method, options) - post[:source] = {} - if payment_method.is_a?(NetworkTokenizationCreditCard) + def add_payment_method(post, payment_method, options, key = :source) + # the key = :destination when this method is called in def credit + post[key] = {} + case payment_method + when NetworkTokenizationCreditCard token_type = token_type_from(payment_method) cryptogram = payment_method.payment_cryptogram eci = payment_method.eci || options[:eci] eci ||= '05' if token_type == 'vts' - post[:source][:type] = 'network_token' - post[:source][:token] = payment_method.number - post[:source][:token_type] = token_type - post[:source][:cryptogram] = cryptogram if cryptogram - post[:source][:eci] = eci if eci - else - post[:source][:type] = 'card' - post[:source][:name] = payment_method.name - post[:source][:number] = payment_method.number - post[:source][:cvv] = payment_method.verification_value - post[:source][:stored] = 'true' if options[:card_on_file] == true + post[key][:type] = 'network_token' + post[key][:token] = payment_method.number + post[key][:token_type] = token_type + post[key][:cryptogram] = cryptogram if cryptogram + post[key][:eci] = eci if eci + when ->(pm) { pm.try(:credit_card?) } + post[key][:type] = 'card' + post[key][:name] = payment_method.name + post[key][:number] = payment_method.number + post[key][:cvv] = payment_method.verification_value unless options[:funds_transfer_type] + post[key][:stored] = 'true' if options[:card_on_file] == true + + # because of the way the key = is implemented in the method signature, some of the destination + # data will be added here, some in the destination specific method below. + # at first i was going to move this, but since this data is coming from the payment method + # i think it makes sense to leave it + if options[:account_holder_type] + post[key][:account_holder] = {} + post[key][:account_holder][:type] = options[:account_holder_type] + + if options[:account_holder_type] == 'corporate' || options[:account_holder_type] == 'government' + post[key][:account_holder][:company_name] = payment_method.name if payment_method.respond_to?(:name) + else + post[key][:account_holder][:first_name] = payment_method.first_name if payment_method.first_name + post[key][:account_holder][:last_name] = payment_method.last_name if payment_method.last_name + end + else + post[key][:first_name] = payment_method.first_name if payment_method.first_name + post[key][:last_name] = payment_method.last_name if payment_method.last_name + end + end + if payment_method.is_a?(String) + if /tok/.match?(payment_method) + post[:type] = 'token' + post[:token] = payment_method + elsif /src/.match?(payment_method) + post[key][:type] = 'id' + post[key][:id] = payment_method + else + add_source(post, options) + end + elsif payment_method.try(:year) + post[key][:expiry_year] = format(payment_method.year, :four_digits) + post[key][:expiry_month] = format(payment_method.month, :two_digits) end - post[:source][:expiry_year] = format(payment_method.year, :four_digits) - post[:source][:expiry_month] = format(payment_method.month, :two_digits) + end + + def add_source(post, options) + post[:source] = {} + post[:source][:type] = options[:source_type] if options[:source_type] + post[:source][:id] = options[:source_id] if options[:source_id] end def add_customer_data(post, options) @@ -149,6 +252,28 @@ def add_customer_data(post, options) end end + # created a separate method for these fields because they should not be included + # in all transaction types that include methods with source and customer fields + def add_extra_customer_data(post, payment_method, options) + post[:source][:phone] = {} + post[:source][:phone][:number] = options[:phone] || options.dig(:billing_address, :phone) || options.dig(:billing_address, :phone_number) + post[:source][:phone][:country_code] = options[:phone_country_code] if options[:phone_country_code] + post[:customer][:name] = payment_method.name if payment_method.respond_to?(:name) + end + + def add_shipping_address(post, options) + if address = options[:shipping_address] + post[:shipping] = {} + post[:shipping][:address] = {} + post[:shipping][:address][:address_line1] = address[:address1] unless address[:address1].blank? + post[:shipping][:address][:address_line2] = address[:address2] unless address[:address2].blank? + post[:shipping][:address][:city] = address[:city] unless address[:city].blank? + post[:shipping][:address][:state] = address[:state] unless address[:state].blank? + post[:shipping][:address][:country] = address[:country] unless address[:country].blank? + post[:shipping][:address][:zip] = address[:zip] unless address[:zip].blank? + end + end + def add_transaction_data(post, options = {}) post[:payment_type] = 'Regular' if options[:transaction_indicator] == 1 post[:payment_type] = 'Recurring' if options[:transaction_indicator] == 2 @@ -156,23 +281,31 @@ def add_transaction_data(post, options = {}) post[:previous_payment_id] = options[:previous_charge_id] if options[:previous_charge_id] end - def add_stored_credential_options(post, options = {}) - return unless options[:stored_credential] + def merchant_initiated_override(post, options) + post[:merchant_initiated] = true + post[:source][:stored] = true + post[:previous_payment_id] = options[:merchant_initiated_transaction_id] + end - case options[:stored_credential][:initial_transaction] - when true + def add_stored_credentials_using_normalized_fields(post, options) + if options[:stored_credential][:initiator] == 'cardholder' post[:merchant_initiated] = false - when false - post[:'source.stored'] = true + else + post[:source][:stored] = true post[:previous_payment_id] = options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id] post[:merchant_initiated] = true end + end + + def add_stored_credential_options(post, options = {}) + return unless options[:stored_credential] - case options[:stored_credential][:reason_type] - when 'recurring', 'installment' - post[:payment_type] = 'Recurring' - when 'unscheduled' - return + post[:payment_type] = 'Recurring' if %w(recurring installment).include? options[:stored_credential][:reason_type] + + if options[:merchant_initiated_transaction_id] + merchant_initiated_override(post, options) + else + add_stored_credentials_using_normalized_fields(post, options) end end @@ -183,6 +316,8 @@ def add_3ds(post, options) post[:success_url] = options[:callback_url] if options[:callback_url] post[:failure_url] = options[:callback_url] if options[:callback_url] post[:'3ds'][:attempt_n3d] = options[:attempt_n3d] if options[:attempt_n3d] + post[:'3ds'][:challenge_indicator] = options[:challenge_indicator] if options[:challenge_indicator] + post[:'3ds'][:exemption] = options[:exemption] if options[:exemption] end if options[:three_d_secure] @@ -190,6 +325,7 @@ def add_3ds(post, options) post[:'3ds'][:cryptogram] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv] post[:'3ds'][:version] = options[:three_d_secure][:version] if options[:three_d_secure][:version] post[:'3ds'][:xid] = options[:three_d_secure][:ds_transaction_id] || options[:three_d_secure][:xid] + post[:'3ds'][:status] = options[:three_d_secure][:authentication_response_status] end end @@ -197,6 +333,81 @@ def add_processing_channel(post, options) post[:processing_channel_id] = options[:processing_channel_id] if options[:processing_channel_id] end + def add_instruction_data(post, options) + post[:instruction] = {} + post[:instruction][:funds_transfer_type] = options[:funds_transfer_type] || 'FD' + post[:instruction][:purpose] = options[:instruction_purpose] if options[:instruction_purpose] + end + + def add_payout_sender_data(post, options) + return unless options[:payout] == true + + post[:sender] = { + # options for type are individual, corporate, or government + type: options[:sender][:type], + # first and last name required if sent by type: individual + first_name: options[:sender][:first_name], + middle_name: options[:sender][:middle_name], + last_name: options[:sender][:last_name], + # company name required if sent by type: corporate or government + company_name: options[:sender][:company_name], + # these are required fields for payout, may not work if address is blank or different than cardholder(option for sender to be a company or government). + # may need to still include in GSF hash. + + address: { + address_line1: options.dig(:sender, :address, :address1), + address_line2: options.dig(:sender, :address, :address2), + city: options.dig(:sender, :address, :city), + state: options.dig(:sender, :address, :state), + country: options.dig(:sender, :address, :country), + zip: options.dig(:sender, :address, :zip) + }.compact, + reference: options[:sender][:reference], + reference_type: options[:sender][:reference_type], + source_of_funds: options[:sender][:source_of_funds], + # identification object is conditional. required when card metadata issuer_country = AR, BR, CO, or PR + # checkout docs say PR (Peru), but PR is puerto rico and PE is Peru so yikes + identification: { + type: options.dig(:sender, :identification, :type), + number: options.dig(:sender, :identification, :number), + issuing_country: options.dig(:sender, :identification, :issuing_country), + date_of_expiry: options.dig(:sender, :identification, :date_of_expiry) + }.compact, + date_of_birth: options[:sender][:date_of_birth], + country_of_birth: options[:sender][:country_of_birth], + nationality: options[:sender][:nationality] + }.compact + end + + def add_payout_destination_data(post, options) + return unless options[:payout] == true + + post[:destination] ||= {} + post[:destination][:account_holder] ||= {} + post[:destination][:account_holder][:email] = options[:destination][:account_holder][:email] if options[:destination][:account_holder][:email] + post[:destination][:account_holder][:date_of_birth] = options[:destination][:account_holder][:date_of_birth] if options[:destination][:account_holder][:date_of_birth] + post[:destination][:account_holder][:country_of_birth] = options[:destination][:account_holder][:country_of_birth] if options[:destination][:account_holder][:country_of_birth] + # below fields only required during a card to card payout + post[:destination][:account_holder][:phone] = {} + post[:destination][:account_holder][:phone][:country_code] = options.dig(:destination, :account_holder, :phone, :country_code) if options.dig(:destination, :account_holder, :phone, :country_code) + post[:destination][:account_holder][:phone][:number] = options.dig(:destination, :account_holder, :phone, :number) if options.dig(:destination, :account_holder, :phone, :number) + + post[:destination][:account_holder][:identification] = {} + post[:destination][:account_holder][:identification][:type] = options.dig(:destination, :account_holder, :identification, :type) if options.dig(:destination, :account_holder, :identification, :type) + post[:destination][:account_holder][:identification][:number] = options.dig(:destination, :account_holder, :identification, :number) if options.dig(:destination, :account_holder, :identification, :number) + post[:destination][:account_holder][:identification][:issuing_country] = options.dig(:destination, :account_holder, :identification, :issuing_country) if options.dig(:destination, :account_holder, :identification, :issuing_country) + post[:destination][:account_holder][:identification][:date_of_expiry] = options.dig(:destination, :account_holder, :identification, :date_of_expiry) if options.dig(:destination, :account_holder, :identification, :date_of_expiry) + + address = options[:billing_address] || options[:address] # destination address will come from the tokenized card billing address + post[:destination][:account_holder][:billing_address] = {} + post[:destination][:account_holder][:billing_address][:address_line1] = address[:address1] unless address[:address1].blank? + post[:destination][:account_holder][:billing_address][:address_line2] = address[:address2] unless address[:address2].blank? + post[:destination][:account_holder][:billing_address][:city] = address[:city] unless address[:city].blank? + post[:destination][:account_holder][:billing_address][:state] = address[:state] unless address[:state].blank? + post[:destination][:account_holder][:billing_address][:country] = address[:country] unless address[:country].blank? + post[:destination][:account_holder][:billing_address][:zip] = address[:zip] unless address[:zip].blank? + end + def add_marketplace_data(post, options) if options[:marketplace] post[:marketplace] = {} @@ -204,55 +415,92 @@ def add_marketplace_data(post, options) end end - def commit(action, post, authorization = nil) + def access_token_header + { + 'Authorization' => "Basic #{Base64.encode64("#{@options[:client_id]}:#{@options[:client_secret]}").delete("\n")}", + 'Content-Type' => 'application/x-www-form-urlencoded' + } + end + + def access_token_url + test? ? TEST_ACCESS_TOKEN_URL : LIVE_ACCESS_TOKEN_URL + end + + def setup_access_token + request = 'grant_type=client_credentials' + response = parse(ssl_post(access_token_url, request, access_token_header)) + response['access_token'] + end + + def commit(action, post, options, authorization = nil, method = :post) begin - raw_response = (action == :verify_payment ? ssl_get("#{base_url}/payments/#{post}", headers) : ssl_post(url(post, action, authorization), post.to_json, headers)) + raw_response = ssl_request(method, url(action, authorization), post.nil? || post.empty? ? nil : post.to_json, headers(action, options)) response = parse(raw_response) response['id'] = response['_links']['payment']['href'].split('/')[-1] if action == :capture && response.key?('_links') + source_id = authorization if action == :unstore rescue ResponseError => e raise unless e.response.code.to_s =~ /4\d\d/ response = parse(e.response.body, error: e.response) end - succeeded = success_from(response) + succeeded = success_from(action, response) - response(action, succeeded, response) + response(action, succeeded, response, source_id) end - def response(action, succeeded, response) - successful_response = succeeded && action == :purchase || action == :authorize - avs_result = successful_response ? avs_result(response) : nil - cvv_result = successful_response ? cvv_result(response) : nil - + def response(action, succeeded, response, source_id = nil) + authorization = authorization_from(response) unless action == :unstore + body = action == :unstore ? { response_code: response.to_s } : response Response.new( succeeded, message_from(succeeded, response), - response, - authorization: authorization_from(response), - error_code: error_code_from(succeeded, response), + body, + authorization: authorization, + error_code: error_code_from(succeeded, body), test: test?, - avs_result: avs_result, - cvv_result: cvv_result + avs_result: avs_result(response), + cvv_result: cvv_result(response) ) end - def headers - { - 'Authorization' => @options[:secret_key], + def headers(action, options) + auth_token = @access_token ? "Bearer #{@access_token}" : @options[:secret_key] + auth_token = @options[:public_key] if action == :tokens + headers = { + 'Authorization' => auth_token, 'Content-Type' => 'application/json;charset=UTF-8' } + headers['Cko-Idempotency-Key'] = options[:idempotency_key] if options[:idempotency_key] + headers end - def url(_post, action, authorization) - if %i[authorize purchase].include?(action) + def tokenize(payment_method, options = {}) + post = {} + add_authorization_type(post, options) + add_payment_method(post, payment_method, options) + add_customer_data(post, options) + commit(:tokens, post[:source], options) + end + + def url(action, authorization) + case action + when :authorize, :purchase, :credit "#{base_url}/payments" - elsif action == :capture + when :unstore, :store + "#{base_url}/instruments/#{authorization}" + when :capture "#{base_url}/payments/#{authorization}/captures" - elsif action == :refund + when :refund "#{base_url}/payments/#{authorization}/refunds" - elsif action == :void + when :void "#{base_url}/payments/#{authorization}/voids" + when :incremental_authorize + "#{base_url}/payments/#{authorization}/authorizations" + when :tokens + "#{base_url}/tokens" + when :verify_payment + "#{base_url}/payments/#{authorization}" else "#{base_url}/payments/#{authorization}/#{action}" end @@ -263,11 +511,11 @@ def base_url end def avs_result(response) - response['source'] && response['source']['avs_check'] ? AVSResult.new(code: response['source']['avs_check']) : nil + response.respond_to?(:dig) && response.dig('source', 'avs_check') ? AVSResult.new(code: response['source']['avs_check']) : nil end def cvv_result(response) - response['source'] && response['source']['cvv_check'] ? CVVResult.new(response['source']['cvv_check']) : nil + response.respond_to?(:dig) && response.dig('source', 'cvv_check') ? CVVResult.new(response['source']['cvv_check']) : nil end def parse(body, error: nil) @@ -282,7 +530,14 @@ def parse(body, error: nil) response end - def success_from(response) + def success_from(action, response) + return response['status'] == 'Pending' if action == :credit + return true if action == :unstore && response == 204 + + store_response = response['token'] || response['id'] + if store_response + return true if (action == :tokens && store_response.match(/tok/)) || (action == :store && store_response.match(/src_/)) + end response['response_summary'] == 'Approved' || response['approved'] == true || !response.key?('response_summary') && response.key?('action_id') end @@ -335,6 +590,16 @@ def token_type_from(payment_method) 'applepay' end end + + def handle_response(response) + case response.code.to_i + # to get the response code after unstore(delete instrument), because the body is nil + when 200...300 + response.body || response.code + else + raise ResponseError.new(response) + end + end end end end diff --git a/lib/active_merchant/billing/gateways/commerce_hub.rb b/lib/active_merchant/billing/gateways/commerce_hub.rb new file mode 100644 index 00000000000..e4d9acca748 --- /dev/null +++ b/lib/active_merchant/billing/gateways/commerce_hub.rb @@ -0,0 +1,370 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class CommerceHubGateway < Gateway + self.test_url = 'https://cert.api.fiservapps.com/ch' + self.live_url = 'https://prod.api.fiservapps.com/ch' + + self.supported_countries = ['US'] + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'https://developer.fiserv.com/product/CommerceHub' + self.display_name = 'CommerceHub' + + STANDARD_ERROR_CODE_MAPPING = {} + + SCHEDULED_REASON_TYPES = %w(recurring installment) + ENDPOINTS = { + 'sale' => '/payments/v1/charges', + 'void' => '/payments/v1/cancels', + 'refund' => '/payments/v1/refunds', + 'vault' => '/payments-vas/v1/tokens', + 'verify' => '/payments-vas/v1/accounts/verification' + } + + def initialize(options = {}) + requires!(options, :api_key, :api_secret, :merchant_id, :terminal_id) + super + end + + def purchase(money, payment, options = {}) + post = {} + options[:capture_flag] = true + options[:create_token] = false + + add_transaction_details(post, options, 'sale') + build_purchase_and_auth_request(post, money, payment, options) + + commit('sale', post, options) + end + + def authorize(money, payment, options = {}) + post = {} + options[:capture_flag] = false + options[:create_token] = false + + add_transaction_details(post, options, 'sale') + build_purchase_and_auth_request(post, money, payment, options) + + commit('sale', post, options) + end + + def capture(money, authorization, options = {}) + post = {} + options[:capture_flag] = true + add_invoice(post, money, options) + add_transaction_details(post, options, 'capture') + add_reference_transaction_details(post, authorization, options, :capture) + + commit('sale', post, options) + end + + def refund(money, authorization, options = {}) + post = {} + add_invoice(post, money, options) if money + add_transaction_details(post, options) + add_reference_transaction_details(post, authorization, options, :refund) + + commit('refund', post, options) + end + + def void(authorization, options = {}) + post = {} + add_transaction_details(post, options) + add_reference_transaction_details(post, authorization, options, :void) + + commit('void', post, options) + end + + def store(credit_card, options = {}) + post = {} + add_payment(post, credit_card, options) + add_billing_address(post, credit_card, options) + add_transaction_details(post, options) + add_transaction_interaction(post, options) + + commit('vault', post, options) + end + + def verify(credit_card, options = {}) + post = {} + add_payment(post, credit_card, options) + add_billing_address(post, credit_card, options) + + commit('verify', post, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: )[a-zA-Z0-9+./=]+), '\1[FILTERED]'). + gsub(%r((Api-Key: )\w+), '\1[FILTERED]'). + gsub(%r(("cardData\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("securityCode\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("cavv\\?":\\?")\w+), '\1[FILTERED]') + end + + private + + def add_transaction_interaction(post, options) + post[:transactionInteraction] = {} + post[:transactionInteraction][:origin] = options[:origin] || 'ECOM' + post[:transactionInteraction][:eciIndicator] = options[:eci_indicator] || 'CHANNEL_ENCRYPTED' + post[:transactionInteraction][:posConditionCode] = options[:pos_condition_code] || 'CARD_NOT_PRESENT_ECOM' + post[:transactionInteraction][:posEntryMode] = (options[:pos_entry_mode] || 'MANUAL') unless options[:encryption_data].present? + post[:transactionInteraction][:additionalPosInformation] = {} + post[:transactionInteraction][:additionalPosInformation][:dataEntrySource] = options[:data_entry_source] || 'UNSPECIFIED' + end + + def add_transaction_details(post, options, action = nil) + details = { + captureFlag: options[:capture_flag], + createToken: options[:create_token], + physicalGoodsIndicator: [true, 'true'].include?(options[:physical_goods_indicator]) + } + + if options[:order_id].present? && action == 'sale' + details[:merchantOrderId] = options[:order_id] + details[:merchantTransactionId] = options[:order_id] + end + + if action != 'capture' + details[:merchantInvoiceNumber] = options[:merchant_invoice_number] || rand.to_s[2..13] + details[:primaryTransactionType] = options[:primary_transaction_type] + details[:accountVerification] = options[:account_verification] + end + + post[:transactionDetails] = details.compact + end + + def add_billing_address(post, payment, options) + return unless billing = options[:billing_address] + + billing_address = {} + if payment.is_a?(CreditCard) + billing_address[:firstName] = payment.first_name if payment.first_name + billing_address[:lastName] = payment.last_name if payment.last_name + end + address = {} + address[:street] = billing[:address1] if billing[:address1] + address[:houseNumberOrName] = billing[:address2] if billing[:address2] + address[:recipientNameOrAddress] = billing[:name] if billing[:name] + address[:city] = billing[:city] if billing[:city] + address[:stateOrProvince] = billing[:state] if billing[:state] + address[:postalCode] = billing[:zip] if billing[:zip] + address[:country] = billing[:country] if billing[:country] + + billing_address[:address] = address unless address.empty? + if billing[:phone_number] + billing_address[:phone] = {} + billing_address[:phone][:phoneNumber] = billing[:phone_number] + end + post[:billingAddress] = billing_address + end + + def add_shipping_address(post, options) + return unless shipping = options[:shipping_address] + + shipping_address = {} + address = {} + address[:street] = shipping[:address1] if shipping[:address1] + address[:houseNumberOrName] = shipping[:address2] if shipping[:address2] + address[:recipientNameOrAddress] = shipping[:name] if shipping[:name] + address[:city] = shipping[:city] if shipping[:city] + address[:stateOrProvince] = shipping[:state] if shipping[:state] + address[:postalCode] = shipping[:zip] if shipping[:zip] + address[:country] = shipping[:country] if shipping[:country] + + shipping_address[:address] = address unless address.empty? + if shipping[:phone_number] + shipping_address[:phone] = {} + shipping_address[:phone][:phoneNumber] = shipping[:phone_number] + end + post[:shippingAddress] = shipping_address + end + + def build_purchase_and_auth_request(post, money, payment, options) + add_invoice(post, money, options) + add_payment(post, payment, options) + add_stored_credentials(post, options) + add_transaction_interaction(post, options) + add_billing_address(post, payment, options) + add_shipping_address(post, options) + end + + def add_reference_transaction_details(post, authorization, options, action = nil) + reference_details = {} + _merchant_reference, transaction_id = authorization.include?('|') ? authorization.split('|') : [nil, authorization] + + reference_details[:referenceTransactionId] = transaction_id + reference_details[:referenceTransactionType] = (options[:reference_transaction_type] || 'CHARGES') unless action == :capture + post[:referenceTransactionDetails] = reference_details.compact + end + + def add_invoice(post, money, options) + post[:amount] = { + total: amount(money).to_f, + currency: options[:currency] || self.default_currency + } + end + + def add_stored_credentials(post, options) + return unless stored_credential = options[:stored_credential] + + post[:storedCredentials] = {} + post[:storedCredentials][:sequence] = stored_credential[:initial_transaction] ? 'FIRST' : 'SUBSEQUENT' + post[:storedCredentials][:initiator] = stored_credential[:initiator] == 'merchant' ? 'MERCHANT' : 'CARD_HOLDER' + post[:storedCredentials][:scheduled] = SCHEDULED_REASON_TYPES.include?(stored_credential[:reason_type]) + post[:storedCredentials][:schemeReferenceTransactionId] = options[:scheme_reference_transaction_id] || stored_credential[:network_transaction_id] + end + + def add_credit_card(source, payment, options) + source[:sourceType] = 'PaymentCard' + source[:card] = {} + source[:card][:cardData] = payment.number + source[:card][:expirationMonth] = format(payment.month, :two_digits) if payment.month + source[:card][:expirationYear] = format(payment.year, :four_digits) if payment.year + if payment.verification_value + source[:card][:securityCode] = payment.verification_value + source[:card][:securityCodeIndicator] = 'PROVIDED' + end + end + + def add_payment_token(source, payment, options) + source[:sourceType] = 'PaymentToken' + source[:tokenData] = payment + source[:tokenSource] = options[:token_source] if options[:token_source] + if options[:card_expiration_month] || options[:card_expiration_year] + source[:card] = {} + source[:card][:expirationMonth] = options[:card_expiration_month] if options[:card_expiration_month] + source[:card][:expirationYear] = options[:card_expiration_year] if options[:card_expiration_year] + end + end + + def add_decrypted_wallet(source, payment, options) + source[:sourceType] = 'DecryptedWallet' + source[:card] = {} + source[:card][:cardData] = payment.number + source[:card][:expirationMonth] = format(payment.month, :two_digits) + source[:card][:expirationYear] = format(payment.year, :four_digits) + source[:cavv] = payment.payment_cryptogram + source[:walletType] = payment.source.to_s.upcase + end + + def add_payment(post, payment, options = {}) + source = {} + case payment + when NetworkTokenizationCreditCard + add_decrypted_wallet(source, payment, options) + when CreditCard + if options[:encryption_data].present? + source[:sourceType] = 'PaymentCard' + source[:encryptionData] = options[:encryption_data] + else + add_credit_card(source, payment, options) + end + when String + add_payment_token(source, payment, options) + end + post[:source] = source + end + + def parse(body) + JSON.parse(body) + end + + def headers(request, options) + time = DateTime.now.strftime('%Q').to_s + client_request_id = options[:client_request_id] || rand.to_s[2..8] + raw_signature = @options[:api_key] + client_request_id.to_s + time + request + hmac = OpenSSL::HMAC.digest('sha256', @options[:api_secret], raw_signature) + signature = Base64.strict_encode64(hmac.to_s).to_s + custom_headers = options.fetch(:headers_identifiers, {}) + { + 'Client-Request-Id' => client_request_id, + 'Api-Key' => @options[:api_key], + 'Timestamp' => time, + 'Accept-Language' => 'application/json', + 'Auth-Token-Type' => 'HMAC', + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => signature + }.merge!(custom_headers) + end + + def add_merchant_details(post) + post[:merchantDetails] = {} + post[:merchantDetails][:terminalId] = @options[:terminal_id] + post[:merchantDetails][:merchantId] = @options[:merchant_id] + end + + def commit(action, parameters, options) + url = (test? ? test_url : live_url) + ENDPOINTS[action] + add_merchant_details(parameters) + response = parse(ssl_post(url, parameters.to_json, headers(parameters.to_json, options))) + + Response.new( + success_from(response, action), + message_from(response, action), + response, + authorization: authorization_from(action, response, options), + test: test?, + error_code: error_code_from(response, action), + avs_result: AVSResult.new(code: get_avs_cvv(response, 'avs')), + cvv_result: CVVResult.new(get_avs_cvv(response, 'cvv')) + ) + end + + def get_avs_cvv(response, type = 'avs') + response.dig( + 'paymentReceipt', + 'processorResponseDetails', + 'bankAssociationDetails', + 'avsSecurityCodeResponse', + 'association', + type == 'avs' ? 'avsCode' : 'securityCodeResponse' + ) + end + + def handle_response(response) + case response.code.to_i + when 200...300, 400, 401, 429 + response.body + else + raise ResponseError.new(response) + end + end + + def success_from(response, action = nil) + return message_from(response, action) == 'VERIFIED' if action == 'verify' + + (response.dig('paymentReceipt', 'processorResponseDetails', 'responseCode') || response.dig('paymentTokens', 0, 'tokenResponseCode')) == '000' + end + + def message_from(response, action = nil) + return response.dig('error', 0, 'message') if response['error'].present? + return response.dig('gatewayResponse', 'transactionState') if action == 'verify' + + response.dig('paymentReceipt', 'processorResponseDetails', 'responseMessage') || response.dig('gatewayResponse', 'transactionType') + end + + def authorization_from(action, response, options) + case action + when 'vault' + response.dig('paymentTokens', 0, 'tokenData') + when 'sale' + [options[:order_id] || '', response.dig('gatewayResponse', 'transactionProcessingDetails', 'transactionId')].join('|') + else + response.dig('gatewayResponse', 'transactionProcessingDetails', 'transactionId') + end + end + + def error_code_from(response, action) + response.dig('error', 0, 'code') unless success_from(response, action) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/credorax.rb b/lib/active_merchant/billing/gateways/credorax.rb index bf632dfa77b..6740a48e337 100644 --- a/lib/active_merchant/billing/gateways/credorax.rb +++ b/lib/active_merchant/billing/gateways/credorax.rb @@ -4,7 +4,7 @@ class CredoraxGateway < Gateway class_attribute :test_url, :live_na_url, :live_eu_url self.display_name = 'Credorax Gateway' - self.homepage_url = 'https://www.credorax.com/' + self.homepage_url = 'https://www.finaro.com/' # NOTE: the IP address you run the remote tests from will need to be # whitelisted by Credorax; contact support@credorax.com as necessary to @@ -26,7 +26,13 @@ class CredoraxGateway < Gateway self.currencies_with_three_decimal_places = %w(BHD IQD JOD KWD LYD OMR TND) self.money_format = :cents - self.supported_cardtypes = %i[visa master maestro american_express] + self.supported_cardtypes = %i[visa master maestro american_express jcb discover diners_club] + + NETWORK_TOKENIZATION_CARD_SOURCE = { + 'apple_pay' => 'applepay', + 'google_pay' => 'googlepay', + 'network_token' => 'vts_mdes_token' + } RESPONSE_MESSAGES = { '00' => 'Approved or completed successfully', @@ -63,26 +69,27 @@ class CredoraxGateway < Gateway '31' => 'Issuer signed-off', '32' => 'Completed partially', '33' => 'Pick-up, expired card', - '34' => 'Suspect Fraud', + '34' => 'Implausible card data', '35' => 'Pick-up, card acceptor contact acquirer', '36' => 'Pick up, card restricted', '37' => 'Pick up, call acquirer security', '38' => 'Pick up, Allowable PIN tries exceeded', - '39' => 'Transaction Not Allowed', + '39' => 'No credit account', '40' => 'Requested function not supported', '41' => 'Lost Card, Pickup', '42' => 'No universal account', '43' => 'Pick up, stolen card', '44' => 'No investment account', + '46' => 'Closed account', '50' => 'Do not renew', - '51' => 'Not sufficient funds', + '51' => 'Insufficient funds', '52' => 'No checking Account', '53' => 'No savings account', '54' => 'Expired card', - '55' => 'Pin incorrect', + '55' => 'Incorrect PIN', '56' => 'No card record', '57' => 'Transaction not allowed for cardholder', - '58' => 'Transaction not allowed for merchant', + '58' => 'Transaction not permitted to terminal', '59' => 'Suspected Fraud', '60' => 'Card acceptor contact acquirer', '61' => 'Exceeds withdrawal amount limit', @@ -93,22 +100,22 @@ class CredoraxGateway < Gateway '66' => 'Call acquirers security department', '67' => 'Card to be picked up at ATM', '68' => 'Response received too late.', - '70' => 'Invalid transaction; contact card issuer', + '70' => 'PIN data required', '71' => 'Decline PIN not changed', '75' => 'Pin tries exceeded', '76' => 'Wrong PIN, number of PIN tries exceeded', '77' => 'Wrong Reference No.', - '78' => 'Record Not Found', - '79' => 'Already reversed', + '78' => 'Blocked, first used/ Record not found', + '79' => 'Declined due to lifecycle event', '80' => 'Network error', - '81' => 'Foreign network error / PIN cryptographic error', - '82' => 'Time out at issuer system', + '81' => 'PIN cryptographic error', + '82' => 'Bad CVV/ Declined due to policy event', '83' => 'Transaction failed', '84' => 'Pre-authorization timed out', '85' => 'No reason to decline', '86' => 'Cannot verify pin', '87' => 'Purchase amount only, no cashback allowed', - '88' => 'MAC sync Error', + '88' => 'Cryptographic failure', '89' => 'Authentication failure', '91' => 'Issuer not available', '92' => 'Unable to route at acquirer Module', @@ -116,9 +123,13 @@ class CredoraxGateway < Gateway '94' => 'Duplicate Transmission', '95' => 'Reconcile error / Auth Not found', '96' => 'System malfunction', + '97' => 'Transaction has been declined by the processor', + 'N3' => 'Cash service not available', + 'N4' => 'Cash request exceeds issuer or approved limit', + 'N7' => 'CVV2 failure', 'R0' => 'Stop Payment Order', 'R1' => 'Revocation of Authorisation Order', - 'R3' => 'Revocation of all Authorisations Order', + 'R3' => 'Revocation of all Authorisation Orders', '1A' => 'Strong Customer Authentication required' } @@ -130,7 +141,7 @@ def initialize(options = {}) def purchase(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) - add_payment_method(post, payment_method) + add_payment_method(post, payment_method, options) add_customer_data(post, options) add_email(post, options) add_3d_secure(post, options) @@ -146,7 +157,7 @@ def purchase(amount, payment_method, options = {}) def authorize(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) - add_payment_method(post, payment_method) + add_payment_method(post, payment_method, options) add_customer_data(post, options) add_email(post, options) add_3d_secure(post, options) @@ -206,13 +217,14 @@ def refund(amount, authorization, options = {}) def credit(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) - add_payment_method(post, payment_method) + add_payment_method(post, payment_method, options) add_customer_data(post, options) add_email(post, options) add_echo(post, options) add_submerchant_id(post, options) add_transaction_type(post, options) add_processor(post, options) + add_customer_name(post, options) commit(:credit, post) end @@ -271,8 +283,9 @@ def add_invoice(post, money, options) 'maestro' => '9' } - def add_payment_method(post, payment_method) - post[:c1] = payment_method.name + def add_payment_method(post, payment_method, options) + post[:c1] = payment_method&.name || '' + add_network_tokenization_card(post, payment_method, options) if payment_method.is_a? NetworkTokenizationCreditCard post[:b2] = CARD_TYPES[payment_method.brand] || '' post[:b1] = payment_method.number post[:b5] = payment_method.verification_value @@ -280,6 +293,13 @@ def add_payment_method(post, payment_method) post[:b3] = format(payment_method.month, :two_digits) end + def add_network_tokenization_card(post, payment_method, options) + post[:b21] = NETWORK_TOKENIZATION_CARD_SOURCE[payment_method.source.to_s] + post[:token_eci] = post[:b21] == 'vts_mdes_token' ? '07' : nil + post[:token_eci] = options[:eci] || payment_method&.eci || (payment_method.brand.to_s == 'master' ? '00' : '07') + post[:token_crypto] = payment_method&.payment_cryptogram if payment_method.source.to_s == 'network_token' + end + def add_stored_credential(post, options) add_transaction_type(post, options) # if :transaction_type option is not passed, then check for :stored_credential options @@ -288,10 +308,11 @@ def add_stored_credential(post, options) if stored_credential[:initiator] == 'merchant' case stored_credential[:reason_type] when 'recurring' - stored_credential[:initial_transaction] ? post[:a9] = '1' : post[:a9] = '2' + post[:a9] = stored_credential[:initial_transaction] ? '1' : '2' when 'installment', 'unscheduled' post[:a9] = '8' end + post[:g6] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id] else post[:a9] = '9' end @@ -324,10 +345,11 @@ def add_email(post, options) def add_recipient(post, options) return unless options[:recipient_street_address] || options[:recipient_city] || options[:recipient_province_code] || options[:recipient_country_code] + recipient_country_code = options[:recipient_country_code]&.length == 3 ? options[:recipient_country_code] : Country.find(options[:recipient_country_code]).code(:alpha3).value if options[:recipient_country_code] post[:j6] = options[:recipient_street_address] if options[:recipient_street_address] post[:j7] = options[:recipient_city] if options[:recipient_city] post[:j8] = options[:recipient_province_code] if options[:recipient_province_code] - post[:j9] = options[:recipient_country_code] if options[:recipient_country_code] + post[:j9] = recipient_country_code end def add_customer_name(post, options) @@ -434,7 +456,7 @@ def add_authorization_details(post, options) capture: '3', authorize_void: '4', refund: '5', - credit: '6', + credit: '35', purchase_void: '7', refund_void: '8', capture_void: '9', @@ -475,11 +497,9 @@ def post_data(action, params, reference_action) end def request_action(action, reference_action) - if reference_action - ACTIONS["#{reference_action}_#{action}".to_sym] - else - ACTIONS[action] - end + return ACTIONS["#{reference_action}_#{action}".to_sym] if reference_action + + ACTIONS[action] end def url diff --git a/lib/active_merchant/billing/gateways/cyber_source.rb b/lib/active_merchant/billing/gateways/cyber_source.rb index 43a3809a0ec..d9a6cc996e1 100644 --- a/lib/active_merchant/billing/gateways/cyber_source.rb +++ b/lib/active_merchant/billing/gateways/cyber_source.rb @@ -22,8 +22,8 @@ class CyberSourceGateway < Gateway self.live_url = 'https://ics2wsa.ic3.com/commerce/1.x/transactionProcessor' # Schema files can be found here: https://ics2ws.ic3.com/commerce/1.x/transactionProcessor/ - TEST_XSD_VERSION = '1.181' - PRODUCTION_XSD_VERSION = '1.181' + TEST_XSD_VERSION = '1.201' + PRODUCTION_XSD_VERSION = '1.201' ECI_BRAND_MAPPING = { visa: 'vbv', master: 'spa', @@ -33,6 +33,15 @@ class CyberSourceGateway < Gateway discover: 'pb', diners_club: 'pb' }.freeze + THREEDS_EXEMPTIONS = { + authentication_outage: 'authenticationOutageExemptionIndicator', + corporate_card: 'secureCorporatePaymentIndicator', + delegated_authentication: 'delegatedAuthenticationExemptionIndicator', + low_risk: 'riskAnalysisExemptionIndicator', + low_value: 'lowValueExemptionIndicator', + stored_credential: 'stored_credential', + trusted_merchant: 'trustedMerchantExemptionIndicator' + } DEFAULT_COLLECTION_INDICATOR = 2 self.supported_cardtypes = %i[visa master american_express discover diners_club jcb dankort maestro elo] @@ -65,6 +74,7 @@ class CyberSourceGateway < Gateway r100: 'Successful transaction', r101: 'Request is missing one or more required fields', r102: 'One or more fields contains invalid data', + r104: 'The merchantReferenceCode sent with this authorization request matches the merchantReferenceCode of another authorization request that you sent in the last 15 minutes.', r110: 'Partial amount was approved', r150: 'General failure', r151: 'The request was received but a server time-out occurred', r152: 'The request was received, but a service timed out', @@ -79,7 +89,9 @@ class CyberSourceGateway < Gateway r209: 'American Express Card Identifiction Digits (CID) did not match', r210: 'The card has reached the credit limit', r211: 'Invalid card verification number', + r220: 'Generic Decline.', r221: "The customer matched an entry on the processor's negative file", + r222: 'customer\'s account is frozen', r230: 'The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the card verification check', r231: 'Invalid account number', r232: 'The card type is not accepted by the payment processor', @@ -97,9 +109,41 @@ class CyberSourceGateway < Gateway r244: 'The bank account number failed the validation check', r246: 'The capture or credit is not voidable because the capture or credit information has already been submitted to your processor', r247: 'You requested a credit for a capture that was previously voided', + r248: 'The boleto request was declined by your processor.', r250: 'The request was received, but a time-out occurred with the payment processor', + r251: 'The Pinless Debit card\'s use frequency or maximum amount per use has been exceeded.', r254: 'Your CyberSource account is prohibited from processing stand-alone refunds', - r255: 'Your CyberSource account is not configured to process the service in the country you specified' + r255: 'Your CyberSource account is not configured to process the service in the country you specified', + r400: 'Soft Decline - Fraud score exceeds threshold.', + r450: 'Apartment number missing or not found.', + r451: 'Insufficient address information.', + r452: 'House/Box number not found on street.', + r453: 'Multiple address matches were found.', + r454: 'P.O. Box identifier not found or out of range.', + r455: 'Route service identifier not found or out of range.', + r456: 'Street name not found in Postal code.', + r457: 'Postal code not found in database.', + r458: 'Unable to verify or correct address.', + r459: 'Multiple addres matches were found (international)', + r460: 'Address match not found (no reason given)', + r461: 'Unsupported character set', + r475: 'The cardholder is enrolled in Payer Authentication. Please authenticate the cardholder before continuing with the transaction.', + r476: 'Encountered a Payer Authentication problem. Payer could not be authenticated.', + r478: 'Strong customer authentication (SCA) is required for this transaction.', + r480: 'The order is marked for review by Decision Manager', + r481: 'The order has been rejected by Decision Manager', + r490: 'Your aggregator or acquirer is not accepting transactions from you at this time.', + r491: 'Your aggregator or acquirer is not accepting this transaction.', + r520: 'Soft Decline - The authorization request was approved by the issuing bank but declined by CyberSource based on your Smart Authorization settings.', + r700: 'The customer matched the Denied Parties List', + r701: 'Export bill_country/ship_country match', + r702: 'Export email_country match', + r703: 'Export hostname_country/ip_country match' + } + + @@payment_solution = { + apple_pay: '001', + google_pay: '012' } # These are the options that can be used when creating a new CyberSource @@ -288,15 +332,17 @@ def build_auth_request(money, creditcard_or_reference, options) add_mdd_fields(xml, options) add_auth_service(xml, creditcard_or_reference, options) add_threeds_services(xml, options) - add_payment_network_token(xml) if network_tokenization?(creditcard_or_reference) add_business_rules_data(xml, creditcard_or_reference, options) + add_airline_data(xml, options) + add_sales_slip_number(xml, options) + add_payment_network_token(xml) if network_tokenization?(creditcard_or_reference) + add_payment_solution(xml, creditcard_or_reference.source) if network_tokenization?(creditcard_or_reference) + add_tax_management_indicator(xml, options) add_stored_credential_subsequent_auth(xml, options) add_issuer_additional_data(xml, options) add_partner_solution_id(xml) add_stored_credential_options(xml, options) add_merchant_description(xml, options) - add_sales_slip_number(xml, options) - add_airline_data(xml, options) xml.target! end @@ -317,6 +363,7 @@ def build_tax_calculation_request(creditcard, options) add_purchase_data(xml, 0, false, options) add_tax_service(xml) add_business_rules_data(xml, creditcard, options) + add_tax_management_indicator(xml, options) xml.target! end @@ -328,8 +375,9 @@ def build_capture_request(money, authorization, options) add_purchase_data(xml, money, true, options) add_other_tax(xml, options) add_mdd_fields(xml, options) - add_capture_service(xml, request_id, request_token) + add_capture_service(xml, request_id, request_token, options) add_business_rules_data(xml, authorization, options) + add_tax_management_indicator(xml, options) add_issuer_additional_data(xml, options) add_merchant_description(xml, options) add_partner_solution_id(xml) @@ -345,21 +393,28 @@ def build_purchase_request(money, payment_method_or_reference, options) add_threeds_2_ucaf_data(xml, payment_method_or_reference, options) add_decision_manager_fields(xml, options) add_mdd_fields(xml, options) - add_sales_slip_number(xml, options) - add_airline_data(xml, options) - if !payment_method_or_reference.is_a?(String) && card_brand(payment_method_or_reference) == 'check' + if (!payment_method_or_reference.is_a?(String) && card_brand(payment_method_or_reference) == 'check') || reference_is_a_check?(payment_method_or_reference) add_check_service(xml) + add_airline_data(xml, options) + add_sales_slip_number(xml, options) + add_tax_management_indicator(xml, options) add_issuer_additional_data(xml, options) add_partner_solution_id(xml) + options[:payment_method] = :check else add_purchase_service(xml, payment_method_or_reference, options) add_threeds_services(xml, options) - add_payment_network_token(xml) if network_tokenization?(payment_method_or_reference) add_business_rules_data(xml, payment_method_or_reference, options) + add_airline_data(xml, options) + add_sales_slip_number(xml, options) + add_payment_network_token(xml) if network_tokenization?(payment_method_or_reference) + add_payment_solution(xml, payment_method_or_reference.source) if network_tokenization?(payment_method_or_reference) + add_tax_management_indicator(xml, options) add_stored_credential_subsequent_auth(xml, options) add_issuer_additional_data(xml, options) add_partner_solution_id(xml) add_stored_credential_options(xml, options) + options[:payment_method] = :credit_card end add_merchant_description(xml, options) @@ -367,6 +422,10 @@ def build_purchase_request(money, payment_method_or_reference, options) xml.target! end + def reference_is_a_check?(payment_method_or_reference) + payment_method_or_reference.is_a?(String) && payment_method_or_reference.split(';')[7] == 'check' + end + def build_void_request(identification, options) order_id, request_id, request_token, action, money, currency = identification.split(';') options[:order_id] = order_id @@ -393,7 +452,9 @@ def build_refund_request(money, identification, options) xml = Builder::XmlMarkup.new indent: 2 add_purchase_data(xml, money, true, options) - add_credit_service(xml, request_id, request_token) + add_credit_service(xml, request_id: request_id, + request_token: request_token, + use_check_service: reference_is_a_check?(identification)) add_partner_solution_id(xml) xml.target! @@ -404,7 +465,7 @@ def build_credit_request(money, creditcard_or_reference, options) add_payment_method_or_subscription(xml, money, creditcard_or_reference, options) add_mdd_fields(xml, options) - add_credit_service(xml) + add_credit_service(xml, use_check_service: creditcard_or_reference.is_a?(Check)) add_issuer_additional_data(xml, options) add_merchant_description(xml, options) @@ -421,11 +482,13 @@ def build_create_subscription_request(payment_method, options) add_address(xml, payment_method, options[:billing_address], options) add_purchase_data(xml, options[:setup_fee] || 0, true, options) if card_brand(payment_method) == 'check' - add_check(xml, payment_method) + add_check(xml, payment_method, options) add_check_payment_method(xml) + options[:payment_method] = :check else add_creditcard(xml, payment_method) add_creditcard_payment_method(xml) + options[:payment_method] = :credit_card end add_subscription(xml, options) if options[:setup_fee] @@ -438,6 +501,7 @@ def build_create_subscription_request(payment_method, options) end add_subscription_create_service(xml, options) add_business_rules_data(xml, payment_method, options) + add_tax_management_indicator(xml, options) xml.target! end @@ -450,6 +514,7 @@ def build_update_subscription_request(reference, creditcard, options) add_subscription(xml, options, reference) add_subscription_update_service(xml, options) add_business_rules_data(xml, creditcard, options) + add_tax_management_indicator(xml, options) xml.target! end @@ -470,11 +535,9 @@ def build_retrieve_subscription_request(reference, options) def add_business_rules_data(xml, payment_method, options) prioritized_options = [options, @options] - unless network_tokenization?(payment_method) - xml.tag! 'businessRules' do - xml.tag!('ignoreAVSResult', 'true') if extract_option(prioritized_options, :ignore_avs).to_s == 'true' - xml.tag!('ignoreCVResult', 'true') if extract_option(prioritized_options, :ignore_cvv).to_s == 'true' - end + xml.tag! 'businessRules' do + xml.tag!('ignoreAVSResult', 'true') if extract_option(prioritized_options, :ignore_avs).to_s == 'true' + xml.tag!('ignoreCVResult', 'true') if extract_option(prioritized_options, :ignore_cvv).to_s == 'true' end end @@ -502,7 +565,7 @@ def add_line_item_data(xml, options) end def add_merchant_data(xml, options) - xml.tag! 'merchantID', @options[:login] + xml.tag! 'merchantID', options[:merchant_id] || @options[:login] xml.tag! 'merchantReferenceCode', options[:order_id] || generate_unique_id xml.tag! 'clientLibrary', 'Ruby Active Merchant' xml.tag! 'clientLibraryVersion', VERSION @@ -512,12 +575,14 @@ def add_merchant_data(xml, options) end def add_merchant_descriptor(xml, options) - return unless options[:merchant_descriptor] || options[:user_po] || options[:taxable] + return unless options[:merchant_descriptor] || options[:user_po] || options[:taxable] || options[:reference_data_code] || options[:invoice_number] xml.tag! 'invoiceHeader' do xml.tag! 'merchantDescriptor', options[:merchant_descriptor] if options[:merchant_descriptor] xml.tag! 'userPO', options[:user_po] if options[:user_po] xml.tag! 'taxable', options[:taxable] if options[:taxable] + xml.tag! 'referenceDataCode', options[:reference_data_code] if options[:reference_data_code] + xml.tag! 'invoiceNumber', options[:invoice_number] if options[:invoice_number] end end @@ -551,10 +616,20 @@ def add_airline_data(xml, options) end end + def add_tax_management_indicator(xml, options) + return unless options[:tax_management_indicator] + + xml.tag! 'taxManagementIndicator', options[:tax_management_indicator] if options[:tax_management_indicator] + end + def add_purchase_data(xml, money = 0, include_grand_total = false, options = {}) xml.tag! 'purchaseTotals' do xml.tag! 'currency', options[:currency] || currency(money) + xml.tag!('discountManagementIndicator', options[:discount_management_indicator]) if options[:discount_management_indicator] + xml.tag!('taxAmount', options[:purchase_tax_amount]) if options[:purchase_tax_amount] xml.tag!('grandTotalAmount', localized_amount(money.to_i, options[:currency] || default_currency)) if include_grand_total + xml.tag!('originalAmount', options[:original_amount]) if options[:original_amount] + xml.tag!('invoiceAmount', options[:invoice_amount]) if options[:invoice_amount] end end @@ -611,6 +686,12 @@ def add_decision_manager_fields(xml, options) end end + def add_payment_solution(xml, source) + return unless (payment_solution = @@payment_solution[source]) + + xml.tag! 'paymentSolution', payment_solution + end + def add_issuer_additional_data(xml, options) return unless options[:issuer_additional_data] @@ -623,6 +704,7 @@ def add_other_tax(xml, options) return unless options[:local_tax_amount] || options[:national_tax_amount] || options[:national_tax_indicator] xml.tag! 'otherTax' do + xml.tag! 'vatTaxRate', options[:vat_tax_rate] if options[:vat_tax_rate] xml.tag! 'localTaxAmount', options[:local_tax_amount] if options[:local_tax_amount] xml.tag! 'nationalTaxAmount', options[:national_tax_amount] if options[:national_tax_amount] xml.tag! 'nationalTaxIndicator', options[:national_tax_indicator] if options[:national_tax_indicator] @@ -640,11 +722,12 @@ def add_mdd_fields(xml, options) end end - def add_check(xml, check) + def add_check(xml, check, options) xml.tag! 'check' do xml.tag! 'accountNumber', check.account_number - xml.tag! 'accountType', check.account_type[0] - xml.tag! 'bankTransitNumber', check.routing_number + xml.tag! 'accountType', check.account_type == 'checking' ? 'C' : 'S' + xml.tag! 'bankTransitNumber', format_routing_number(check.routing_number, options) + xml.tag! 'secCode', options[:sec_code] if options[:sec_code] end end @@ -662,15 +745,28 @@ def add_auth_service(xml, payment_method, options) xml.tag! 'ccAuthService', { 'run' => 'true' } do if options[:three_d_secure] add_normalized_threeds_2_data(xml, payment_method, options) + add_threeds_exemption_data(xml, options) if options[:three_ds_exemption_type] else indicator = options[:commerce_indicator] || stored_credential_commerce_indicator(options) xml.tag!('commerceIndicator', indicator) if indicator end xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] + xml.tag!('mobileRemotePaymentType', options[:mobile_remote_payment_type]) if options[:mobile_remote_payment_type] end end end + def add_threeds_exemption_data(xml, options) + return unless options[:three_ds_exemption_type] + + exemption = options[:three_ds_exemption_type].to_sym + + case exemption + when :authentication_outage, :corporate_card, :delegated_authentication, :low_risk, :low_value, :trusted_merchant + xml.tag!(THREEDS_EXEMPTIONS[exemption], '1') + end + end + def add_incremental_auth_service(xml, authorization, options) xml.tag! 'ccIncrementalAuthService', { 'run' => 'true' } do xml.tag! 'authRequestID', authorization @@ -732,26 +828,35 @@ def network_tokenization?(payment_method) payment_method.is_a?(NetworkTokenizationCreditCard) end + def subsequent_nt_apple_pay_auth(source, options) + return unless options[:stored_credential] || options[:stored_credential_overrides] + return unless @@payment_solution[source] + + options.dig(:stored_credential_overrides, :subsequent_auth) || options.dig(:stored_credential, :initiator) == 'merchant' + end + def add_auth_network_tokenization(xml, payment_method, options) return unless network_tokenization?(payment_method) + commerce_indicator = 'internet' if subsequent_nt_apple_pay_auth(payment_method.source, options) + brand = card_brand(payment_method).to_sym case brand when :visa xml.tag! 'ccAuthService', { 'run' => 'true' } do - xml.tag!('cavv', payment_method.payment_cryptogram) - xml.tag!('commerceIndicator', ECI_BRAND_MAPPING[brand]) - xml.tag!('xid', payment_method.payment_cryptogram) + xml.tag!('cavv', payment_method.payment_cryptogram) unless commerce_indicator + xml.commerceIndicator commerce_indicator.nil? ? ECI_BRAND_MAPPING[brand] : commerce_indicator + xml.tag!('xid', payment_method.payment_cryptogram) unless commerce_indicator xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] end when :master xml.tag! 'ucaf' do - xml.tag!('authenticationData', payment_method.payment_cryptogram) + xml.tag!('authenticationData', payment_method.payment_cryptogram) unless commerce_indicator xml.tag!('collectionIndicator', DEFAULT_COLLECTION_INDICATOR) end xml.tag! 'ccAuthService', { 'run' => 'true' } do - xml.tag!('commerceIndicator', ECI_BRAND_MAPPING[brand]) + xml.commerceIndicator commerce_indicator.nil? ? ECI_BRAND_MAPPING[brand] : commerce_indicator xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] end when :american_express @@ -759,9 +864,11 @@ def add_auth_network_tokenization(xml, payment_method, options) xml.tag! 'ccAuthService', { 'run' => 'true' } do xml.tag!('cavv', Base64.encode64(cryptogram[0...20])) xml.tag!('commerceIndicator', ECI_BRAND_MAPPING[brand]) - xml.tag!('xid', Base64.encode64(cryptogram[20...40])) + xml.tag!('xid', Base64.encode64(cryptogram[20...40])) if cryptogram.bytes.count > 20 xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] end + else + raise ArgumentError.new("Payment method #{brand} is not supported, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html") end end @@ -771,10 +878,11 @@ def add_payment_network_token(xml) end end - def add_capture_service(xml, request_id, request_token) + def add_capture_service(xml, request_id, request_token, options) xml.tag! 'ccCaptureService', { 'run' => 'true' } do xml.tag! 'authRequestID', request_id xml.tag! 'authRequestToken', request_token + xml.tag! 'gratuityAmount', options[:gratuity_amount] if options[:gratuity_amount] xml.tag! 'reconciliationID', options[:reconciliation_id] if options[:reconciliation_id] end end @@ -800,10 +908,14 @@ def add_auth_reversal_service(xml, request_id, request_token) end end - def add_credit_service(xml, request_id = nil, request_token = nil) - xml.tag! 'ccCreditService', { 'run' => 'true' } do - xml.tag! 'captureRequestID', request_id if request_id - xml.tag! 'captureRequestToken', request_token if request_token + def add_credit_service(xml, options = {}) + service = options[:use_check_service] ? 'ecCreditService' : 'ccCreditService' + request_tag = options[:use_check_service] ? 'debitRequestID' : 'captureRequestID' + options.delete :request_token if options[:use_check_service] + + xml.tag! service, { 'run' => 'true' } do + xml.tag! request_tag, options[:request_id] if options[:request_id] + xml.tag! 'captureRequestToken', options[:request_token] if options[:request_token] end end @@ -870,7 +982,7 @@ def add_payment_method_or_subscription(xml, money, payment_method_or_reference, add_address(xml, payment_method_or_reference, options[:billing_address], options) add_purchase_data(xml, money, true, options) add_installments(xml, options) - add_check(xml, payment_method_or_reference) + add_check(xml, payment_method_or_reference, options) else add_address(xml, payment_method_or_reference, options[:billing_address], options) add_address(xml, payment_method_or_reference, options[:shipping_address], options, true) @@ -882,12 +994,15 @@ def add_payment_method_or_subscription(xml, money, payment_method_or_reference, end def add_installments(xml, options) - return unless options[:installment_total_count] + return unless %i[installment_total_count installment_total_amount installment_plan_type first_installment_date installment_annual_interest_rate installment_grace_period_duration].any? { |gsf| options.include?(gsf) } xml.tag! 'installment' do - xml.tag! 'totalCount', options[:installment_total_count] + xml.tag!('totalCount', options[:installment_total_count]) if options[:installment_total_count] + xml.tag!('totalAmount', options[:installment_total_amount]) if options[:installment_total_amount] xml.tag!('planType', options[:installment_plan_type]) if options[:installment_plan_type] xml.tag!('firstInstallmentDate', options[:first_installment_date]) if options[:first_installment_date] + xml.tag!('annualInterestRate', options[:installment_annual_interest_rate]) if options[:installment_annual_interest_rate] + xml.tag!('gracePeriodDuration', options[:installment_grace_period_duration]) if options[:installment_grace_period_duration] end end @@ -920,7 +1035,7 @@ def add_stored_credential_options(xml, options = {}) stored_credential_subsequent_auth_first = 'true' if options.dig(:stored_credential, :initial_transaction) stored_credential_transaction_id = options.dig(:stored_credential, :network_transaction_id) if options.dig(:stored_credential, :initiator) == 'merchant' - stored_credential_subsequent_auth_stored_cred = 'true' if options.dig(:stored_credential, :initiator) == 'cardholder' && !options.dig(:stored_credential, :initial_transaction) || options.dig(:stored_credential, :initiator) == 'merchant' && options.dig(:stored_credential, :reason_type) == 'unscheduled' + stored_credential_subsequent_auth_stored_cred = 'true' if subsequent_cardholder_initiated_transaction?(options) || unscheduled_merchant_initiated_transaction?(options) || threeds_stored_credential_exemption?(options) override_subsequent_auth_first = options.dig(:stored_credential_overrides, :subsequent_auth_first) override_subsequent_auth_transaction_id = options.dig(:stored_credential_overrides, :subsequent_auth_transaction_id) @@ -931,6 +1046,18 @@ def add_stored_credential_options(xml, options = {}) xml.subsequentAuthStoredCredential override_subsequent_auth_stored_cred.nil? ? stored_credential_subsequent_auth_stored_cred : override_subsequent_auth_stored_cred end + def subsequent_cardholder_initiated_transaction?(options) + options.dig(:stored_credential, :initiator) == 'cardholder' && !options.dig(:stored_credential, :initial_transaction) + end + + def unscheduled_merchant_initiated_transaction?(options) + options.dig(:stored_credential, :initiator) == 'merchant' && options.dig(:stored_credential, :reason_type) == 'unscheduled' + end + + def threeds_stored_credential_exemption?(options) + options[:three_ds_exemption_type] == THREEDS_EXEMPTIONS[:stored_credential] + end + def add_partner_solution_id(xml) return unless application_id @@ -981,12 +1108,26 @@ def commit(request, action, amount, options) message = message_from(response) authorization = success || in_fraud_review?(response) ? authorization_from(response, action, amount, options) : nil - Response.new(success, message, response, + message = auto_void?(authorization_from(response, action, amount, options), response, message, options) + + Response.new( + success, + message, + response, test: test?, authorization: authorization, fraud_review: in_fraud_review?(response), avs_result: { code: response[:avsCode] }, - cvv_result: response[:cvCode]) + cvv_result: response[:cvCode] + ) + end + + def auto_void?(authorization, response, message, options = {}) + return message unless response[:reasonCode] == '230' && options[:auto_void_230] + + response = void(authorization, options) + response&.success? ? message += ' - transaction has been auto-voided.' : message += ' - transaction could not be auto-voided.' + message end # Parse the SOAP response @@ -1033,7 +1174,7 @@ def reason_message(reason_code) def authorization_from(response, action, amount, options) [options[:order_id], response[:requestID], response[:requestToken], action, amount, - options[:currency], response[:subscriptionID]].join(';') + options[:currency], response[:subscriptionID], options[:payment_method]].join(';') end def in_fraud_review?(response) @@ -1057,6 +1198,10 @@ def message_from(response) def eligible_for_zero_auth?(payment_method, options = {}) payment_method.is_a?(CreditCard) && options[:zero_amount_auth] end + + def format_routing_number(routing_number, options) + options[:currency] == 'CAD' && routing_number.length > 8 ? routing_number[-8..-1] : routing_number + end end end end diff --git a/lib/active_merchant/billing/gateways/cyber_source/cyber_source_common.rb b/lib/active_merchant/billing/gateways/cyber_source/cyber_source_common.rb new file mode 100644 index 00000000000..9e37a41fca7 --- /dev/null +++ b/lib/active_merchant/billing/gateways/cyber_source/cyber_source_common.rb @@ -0,0 +1,36 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + module CyberSourceCommon + def check_billing_field_value(default, submitted) + if submitted.nil? + nil + elsif submitted.blank? + default + else + submitted + end + end + + def address_names(address_name, payment_method) + names = split_names(address_name) + return names if names.any?(&:present?) + + [ + payment_method&.first_name, + payment_method&.last_name + ] + end + + def lookup_country_code(country_field) + return unless country_field.present? + + country_code = Country.find(country_field) + country_code&.code(:alpha2) + end + + def eligible_for_zero_auth?(payment_method, options = {}) + payment_method.is_a?(CreditCard) && options[:zero_amount_auth] + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/cyber_source_rest.rb b/lib/active_merchant/billing/gateways/cyber_source_rest.rb new file mode 100644 index 00000000000..28c4d9d6f12 --- /dev/null +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -0,0 +1,454 @@ +require 'active_merchant/billing/gateways/cyber_source/cyber_source_common' + +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class CyberSourceRestGateway < Gateway + include ActiveMerchant::Billing::CyberSourceCommon + + self.test_url = 'https://apitest.cybersource.com' + self.live_url = 'https://api.cybersource.com' + + self.supported_countries = ActiveMerchant::Billing::CyberSourceGateway.supported_countries + self.default_currency = 'USD' + self.currencies_without_fractions = ActiveMerchant::Billing::CyberSourceGateway.currencies_without_fractions + + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb maestro elo union_pay cartes_bancaires mada] + + self.homepage_url = 'http://www.cybersource.com' + self.display_name = 'Cybersource REST' + + CREDIT_CARD_CODES = { + american_express: '003', + cartes_bancaires: '036', + dankort: '034', + diners_club: '005', + discover: '004', + elo: '054', + jcb: '007', + maestro: '042', + master: '002', + unionpay: '062', + visa: '001' + } + + PAYMENT_SOLUTION = { + apple_pay: '001', + google_pay: '012' + } + + def initialize(options = {}) + requires!(options, :merchant_id, :public_key, :private_key) + super + end + + def purchase(money, payment, options = {}) + authorize(money, payment, options, true) + end + + def authorize(money, payment, options = {}, capture = false) + post = build_auth_request(money, payment, options) + post[:processingInformation][:capture] = true if capture + + commit('payments', post, options) + end + + def capture(money, authorization, options = {}) + payment = authorization.split('|').first + post = build_reference_request(money, options) + + commit("payments/#{payment}/captures", post, options) + end + + def refund(money, authorization, options = {}) + payment = authorization.split('|').first + post = build_reference_request(money, options) + commit("payments/#{payment}/refunds", post, options) + end + + def credit(money, payment, options = {}) + post = build_credit_request(money, payment, options) + commit('credits', post) + end + + def void(authorization, options = {}) + payment, amount = authorization.split('|') + post = build_void_request(amount) + commit("payments/#{payment}/reversals", post) + end + + def verify(credit_card, options = {}) + amount = eligible_for_zero_auth?(credit_card, options) ? 0 : 100 + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(amount, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(/(\\?"number\\?":\\?")\d+/, '\1[FILTERED]'). + gsub(/(\\?"routingNumber\\?":\\?")\d+/, '\1[FILTERED]'). + gsub(/(\\?"securityCode\\?":\\?")\d+/, '\1[FILTERED]'). + gsub(/(signature=")[^"]*/, '\1[FILTERED]'). + gsub(/(keyid=")[^"]*/, '\1[FILTERED]'). + gsub(/(Digest: SHA-256=)[\w\/\+=]*/, '\1[FILTERED]') + end + + private + + def build_void_request(amount = nil) + { reversalInformation: { amountDetails: { totalAmount: nil } } }.tap do |post| + add_reversal_amount(post, amount.to_i) if amount.present? + end.compact + end + + def build_auth_request(amount, payment, options) + { clientReferenceInformation: {}, paymentInformation: {}, orderInformation: {} }.tap do |post| + add_customer_id(post, options) + add_code(post, options) + add_payment(post, payment, options) + add_mdd_fields(post, options) + add_amount(post, amount, options) + add_address(post, payment, options[:billing_address], options, :billTo) + add_address(post, payment, options[:shipping_address], options, :shipTo) + add_business_rules_data(post, payment, options) + add_partner_solution_id(post) + add_stored_credentials(post, payment, options) + end.compact + end + + def build_reference_request(amount, options) + { clientReferenceInformation: {}, orderInformation: {} }.tap do |post| + add_code(post, options) + add_mdd_fields(post, options) + add_amount(post, amount, options) + add_partner_solution_id(post) + end.compact + end + + def build_credit_request(amount, payment, options) + { clientReferenceInformation: {}, paymentInformation: {}, orderInformation: {} }.tap do |post| + add_code(post, options) + add_credit_card(post, payment) + add_mdd_fields(post, options) + add_amount(post, amount, options) + add_address(post, payment, options[:billing_address], options, :billTo) + add_merchant_description(post, options) + end.compact + end + + def add_code(post, options) + return unless options[:order_id].present? + + post[:clientReferenceInformation][:code] = options[:order_id] + end + + def add_customer_id(post, options) + return unless options[:customer_id].present? + + post[:paymentInformation][:customer] = { customerId: options[:customer_id] } + end + + def add_reversal_amount(post, amount) + currency = options[:currency] || currency(amount) + + post[:reversalInformation][:amountDetails] = { + totalAmount: localized_amount(amount, currency) + } + end + + def add_amount(post, amount, options) + currency = options[:currency] || currency(amount) + post[:orderInformation][:amountDetails] = { + totalAmount: localized_amount(amount, currency), + currency: currency + } + end + + def add_ach(post, payment) + post[:paymentInformation][:bank] = { + account: { + type: payment.account_type == 'checking' ? 'C' : 'S', + number: payment.account_number + }, + routingNumber: payment.routing_number + } + end + + def add_payment(post, payment, options) + post[:processingInformation] = {} + if payment.is_a?(NetworkTokenizationCreditCard) + add_network_tokenization_card(post, payment, options) + elsif payment.is_a?(Check) + add_ach(post, payment) + else + add_credit_card(post, payment) + end + end + + def add_network_tokenization_card(post, payment, options) + post[:processingInformation][:paymentSolution] = PAYMENT_SOLUTION[payment.source] + post[:processingInformation][:commerceIndicator] = 'internet' unless card_brand(payment) == 'jcb' + + post[:paymentInformation][:tokenizedCard] = { + number: payment.number, + expirationMonth: payment.month, + expirationYear: payment.year, + cryptogram: payment.payment_cryptogram, + transactionType: '1', + type: CREDIT_CARD_CODES[card_brand(payment).to_sym] + } + + if card_brand(payment) == 'master' + post[:consumerAuthenticationInformation] = { + ucafAuthenticationData: payment.payment_cryptogram, + ucafCollectionIndicator: '2' + } + else + post[:consumerAuthenticationInformation] = { cavv: payment.payment_cryptogram } + end + end + + def add_credit_card(post, creditcard) + post[:paymentInformation][:card] = { + number: creditcard.number, + expirationMonth: format(creditcard.month, :two_digits), + expirationYear: format(creditcard.year, :four_digits), + securityCode: creditcard.verification_value, + type: CREDIT_CARD_CODES[card_brand(creditcard).to_sym] + } + end + + def add_address(post, payment_method, address, options, address_type) + return unless address.present? + + first_name, last_name = address_names(address[:name], payment_method) + + post[:orderInformation][address_type] = { + firstName: first_name, + lastName: last_name, + address1: address[:address1], + address2: address[:address2], + locality: address[:city], + administrativeArea: address[:state], + postalCode: address[:zip], + country: lookup_country_code(address[:country])&.value, + email: options[:email].presence || 'null@cybersource.com', + phoneNumber: address[:phone] + # merchantTaxID: ship_to ? options[:merchant_tax_id] : nil, + # company: address[:company], + # companyTaxID: address[:companyTaxID], + # ipAddress: options[:ip], + # driversLicenseNumber: options[:drivers_license_number], + # driversLicenseState: options[:drivers_license_state], + }.compact + end + + def add_merchant_description(post, options) + return unless options[:merchant_descriptor_name] || options[:merchant_descriptor_address1] || options[:merchant_descriptor_locality] + + merchant = post[:merchantInformation][:merchantDescriptor] = {} + merchant[:name] = options[:merchant_descriptor_name] if options[:merchant_descriptor_name] + merchant[:address1] = options[:merchant_descriptor_address1] if options[:merchant_descriptor_address1] + merchant[:locality] = options[:merchant_descriptor_locality] if options[:merchant_descriptor_locality] + end + + def add_stored_credentials(post, payment, options) + return unless stored_credential = options[:stored_credential] + + options = stored_credential_options(stored_credential, options.fetch(:reason_code, '')) + post[:processingInformation][:commerceIndicator] = options.fetch(:transaction_type, 'internet') + stored_credential[:initial_transaction] ? initial_transaction(post, options) : subsequent_transaction(post, options) + end + + def stored_credential_options(options, reason_code) + transaction_type = options[:reason_type] + transaction_type = 'install' if transaction_type == 'installment' + initiator = options[:initiator] if options[:initiator] + initiator = 'customer' if initiator == 'cardholder' + stored_on_file = options[:reason_type] == 'recurring' + options.merge({ + transaction_type: transaction_type, + initiator: initiator, + reason_code: reason_code, + stored_on_file: stored_on_file + }) + end + + def add_processing_information(initiator, merchant_initiated_transaction_hash = {}) + { + authorizationOptions: { + initiator: { + type: initiator, + merchantInitiatedTransaction: merchant_initiated_transaction_hash, + storedCredentialUsed: true + } + } + }.compact + end + + def initial_transaction(post, options) + processing_information = add_processing_information(options[:initiator], { + reason: options[:reason_code] + }) + + post[:processingInformation].merge!(processing_information) + end + + def subsequent_transaction(post, options) + network_transaction_id = options[:network_transaction_id] || options.dig(:stored_credential, :network_transaction_id) || '' + processing_information = add_processing_information(options[:initiator], { + originalAuthorizedAmount: post.dig(:orderInformation, :amountDetails, :totalAmount), + previousTransactionID: network_transaction_id, + reason: options[:reason_code], + storedCredentialUsed: options[:stored_on_file] + }) + post[:processingInformation].merge!(processing_information) + end + + def network_transaction_id_from(response) + response.dig('processorInformation', 'networkTransactionId') + end + + def url(action) + "#{(test? ? test_url : live_url)}/pts/v2/#{action}" + end + + def host + URI.parse(url('')).host + end + + def parse(body) + JSON.parse(body) + end + + def commit(action, post, options = {}) + add_reconciliation_id(post, options) + add_sec_code(post, options) + add_invoice_number(post, options) + response = parse(ssl_post(url(action), post.to_json, auth_headers(action, options, post))) + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + avs_result: AVSResult.new(code: response.dig('processorInformation', 'avs', 'code')), + # cvv_result: CVVResult.new(response['some_cvv_response_key']), + network_transaction_id: network_transaction_id_from(response), + test: test?, + error_code: error_code_from(response) + ) + rescue ActiveMerchant::ResponseError => e + response = e.response.body.present? ? parse(e.response.body) : { 'response' => { 'rmsg' => e.response.msg } } + message = response.dig('response', 'rmsg') || response.dig('message') + Response.new(false, message, response, test: test?) + end + + def success_from(response) + %w(AUTHORIZED PENDING REVERSED).include?(response['status']) + end + + def message_from(response) + return response['status'] if success_from(response) + + response['errorInformation']['message'] || response['message'] + end + + def authorization_from(response) + id = response['id'] + has_amount = response['orderInformation'] && response['orderInformation']['amountDetails'] && response['orderInformation']['amountDetails']['authorizedAmount'] + amount = response['orderInformation']['amountDetails']['authorizedAmount'].delete('.') if has_amount + + return id if amount.blank? + + [id, amount].join('|') + end + + def error_code_from(response) + response['errorInformation']['reason'] unless success_from(response) + end + + # This implementation follows the Cybersource guide on how create the request signature, see: + # https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/GenerateHeader/httpSignatureAuthentication.html + def get_http_signature(resource, digest, http_method = 'post', gmtdatetime = Time.now.httpdate) + string_to_sign = { + host: host, + date: gmtdatetime, + "(request-target)": "#{http_method} /pts/v2/#{resource}", + digest: digest, + "v-c-merchant-id": @options[:merchant_id] + }.map { |k, v| "#{k}: #{v}" }.join("\n").force_encoding(Encoding::UTF_8) + + { + keyid: @options[:public_key], + algorithm: 'HmacSHA256', + headers: "host date (request-target)#{digest.present? ? ' digest' : ''} v-c-merchant-id", + signature: sign_payload(string_to_sign) + }.map { |k, v| %{#{k}="#{v}"} }.join(', ') + end + + def sign_payload(payload) + decoded_key = Base64.decode64(@options[:private_key]) + Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', decoded_key, payload)) + end + + def auth_headers(action, options, post, http_method = 'post') + digest = "SHA-256=#{Digest::SHA256.base64digest(post.to_json)}" if post.present? + date = Time.now.httpdate + + { + 'Accept' => 'application/hal+json;charset=utf-8', + 'Content-Type' => 'application/json;charset=utf-8', + 'V-C-Merchant-Id' => options[:merchant_id] || @options[:merchant_id], + 'Date' => date, + 'Host' => host, + 'Signature' => get_http_signature(action, digest, http_method, date), + 'Digest' => digest + } + end + + def add_business_rules_data(post, payment, options) + post[:processingInformation][:authorizationOptions] = {} + post[:processingInformation][:authorizationOptions][:ignoreAvsResult] = 'true' if options[:ignore_avs].to_s == 'true' + post[:processingInformation][:authorizationOptions][:ignoreCvResult] = 'true' if options[:ignore_cvv].to_s == 'true' + end + + def add_mdd_fields(post, options) + mdd_fields = options.select { |k, v| k.to_s.start_with?('mdd_field') && v.present? } + return unless mdd_fields.present? + + post[:merchantDefinedInformation] = mdd_fields.map do |key, value| + { key: key, value: value } + end + end + + def add_reconciliation_id(post, options) + return unless options[:reconciliation_id].present? + + post[:clientReferenceInformation][:reconciliationId] = options[:reconciliation_id] + end + + def add_sec_code(post, options) + return unless options[:sec_code].present? + + post[:processingInformation][:bankTransferOptions] = { secCode: options[:sec_code] } + end + + def add_invoice_number(post, options) + return unless options[:invoice_number].present? + + post[:orderInformation][:invoiceDetails] = { invoiceNumber: options[:invoice_number] } + end + + def add_partner_solution_id(post) + return unless application_id + + post[:clientReferenceInformation][:partner] = { solutionId: application_id } + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/d_local.rb b/lib/active_merchant/billing/gateways/d_local.rb index a3e6c632b51..6a172e77ee4 100644 --- a/lib/active_merchant/billing/gateways/d_local.rb +++ b/lib/active_merchant/billing/gateways/d_local.rb @@ -4,7 +4,7 @@ class DLocalGateway < Gateway self.test_url = 'https://sandbox.dlocal.com' self.live_url = 'https://api.dlocal.com' - self.supported_countries = %w[AR BD BO BR CL CM CN CO CR DO EC EG GH IN ID KE MY MX MA NG PA PY PE PH SN ZA TR UY VN] + self.supported_countries = %w[AR BD BO BR CL CM CN CO CR DO EC EG GH GT IN ID JP KE MY MX MA NG PA PY PE PH SN SV TH TR TZ UG UY VN ZA] self.default_currency = 'USD' self.supported_cardtypes = %i[visa master american_express discover jcb diners_club maestro naranja cabal elo alia carnet] @@ -58,10 +58,21 @@ def verify(credit_card, options = {}) authorize(0, credit_card, options.merge(verify: 'true')) end + def inquire(authorization, options = {}) + post = {} + post[:payment_id] = authorization + action = authorization ? 'status' : 'orders' + commit(action, post, options) + end + def supports_scrubbing? true end + def supports_network_tokenization? + true + end + def scrub(transcript) transcript. gsub(%r((X-Trans-Key: )\w+), '\1[FILTERED]'). @@ -80,6 +91,7 @@ def add_auth_purchase_params(post, money, card, action, options) add_card(post, card, action, options) add_additional_data(post, options) post[:order_id] = options[:order_id] || generate_unique_id + post[:original_order_id] = options[:original_order_id] if options[:original_order_id] post[:description] = options[:description] if options[:description] end @@ -147,16 +159,36 @@ def parse_house_number(address) def add_card(post, card, action, options = {}) post[:card] = {} + if card.is_a?(NetworkTokenizationCreditCard) + post[:card][:network_token] = card.number + post[:card][:cryptogram] = card.payment_cryptogram + post[:card][:eci] = card.eci + # used case of Network Token: 'CARD_ON_FILE', 'SUBSCRIPTION', 'UNSCHEDULED_CARD_ON_FILE' + if options.dig(:stored_credential, :reason_type) == 'unscheduled' + if options.dig(:stored_credential, :initiator) == 'merchant' + post[:card][:stored_credential_type] = 'UNSCHEDULED_CARD_ON_FILE' + else + post[:card][:stored_credential_type] = 'CARD_ON_FILE' + end + else + post[:card][:stored_credential_type] = 'SUBSCRIPTION' + end + # required for MC debit recurrent in BR 'USED'(subsecuence Payments) . 'FIRST' an inital payment + post[:card][:stored_credential_usage] = (options[:stored_credential][:initial_transaction] ? 'FIRST' : 'USED') if options[:stored_credential] + else + post[:card][:number] = card.number + post[:card][:cvv] = card.verification_value + end + post[:card][:holder_name] = card.name post[:card][:expiration_month] = card.month post[:card][:expiration_year] = card.year - post[:card][:number] = card.number - post[:card][:cvv] = card.verification_value post[:card][:descriptor] = options[:dynamic_descriptor] if options[:dynamic_descriptor] post[:card][:capture] = (action == 'purchase') post[:card][:installments] = options[:installments] if options[:installments] post[:card][:installments_id] = options[:installments_id] if options[:installments_id] post[:card][:force_type] = options[:force_type].to_s.upcase if options[:force_type] + post[:card][:save] = options[:save] if options[:save] end def parse(body) @@ -170,7 +202,11 @@ def commit(action, parameters, options = {}) url = url(action, parameters, options) post = post_data(action, parameters) begin - raw = ssl_post(url, post, headers(post, options)) + raw = if %w(status orders).include?(action) + ssl_get(url, headers(nil, options)) + else + ssl_post(url, post, headers(post, options)) + end response = parse(raw) rescue ResponseError => e raw = e.response.body @@ -229,6 +265,10 @@ def endpoint(action, parameters, options) 'payments' when 'void' "payments/#{parameters[:authorization_id]}/cancel" + when 'status' + "payments/#{parameters[:payment_id]}/status" + when 'orders' + "orders/#{options[:order_id]}" end end @@ -242,7 +282,7 @@ def headers(post, options = {}) 'X-Version' => '2.1', 'Authorization' => signature(post, timestamp) } - headers.merge('X-Idempotency-Key' => options[:idempotency_key]) if options[:idempotency_key] + headers['X-Idempotency-Key'] = options[:idempotency_key] if options[:idempotency_key] headers end diff --git a/lib/active_merchant/billing/gateways/data_cash.rb b/lib/active_merchant/billing/gateways/data_cash.rb index 46058035510..b0bbe98f266 100644 --- a/lib/active_merchant/billing/gateways/data_cash.rb +++ b/lib/active_merchant/billing/gateways/data_cash.rb @@ -235,23 +235,23 @@ def add_credit_card(xml, credit_card, address) # a predefined one xml.tag! :ExtendedPolicy do xml.tag! :cv2_policy, - notprovided: POLICY_REJECT, - notchecked: POLICY_REJECT, - matched: POLICY_ACCEPT, - notmatched: POLICY_REJECT, - partialmatch: POLICY_REJECT + notprovided: POLICY_REJECT, + notchecked: POLICY_REJECT, + matched: POLICY_ACCEPT, + notmatched: POLICY_REJECT, + partialmatch: POLICY_REJECT xml.tag! :postcode_policy, - notprovided: POLICY_ACCEPT, - notchecked: POLICY_ACCEPT, - matched: POLICY_ACCEPT, - notmatched: POLICY_REJECT, - partialmatch: POLICY_ACCEPT + notprovided: POLICY_ACCEPT, + notchecked: POLICY_ACCEPT, + matched: POLICY_ACCEPT, + notmatched: POLICY_REJECT, + partialmatch: POLICY_ACCEPT xml.tag! :address_policy, - notprovided: POLICY_ACCEPT, - notchecked: POLICY_ACCEPT, - matched: POLICY_ACCEPT, - notmatched: POLICY_REJECT, - partialmatch: POLICY_ACCEPT + notprovided: POLICY_ACCEPT, + notchecked: POLICY_ACCEPT, + matched: POLICY_ACCEPT, + notmatched: POLICY_REJECT, + partialmatch: POLICY_ACCEPT end end end @@ -260,9 +260,13 @@ def add_credit_card(xml, credit_card, address) def commit(request) response = parse(ssl_post(test? ? self.test_url : self.live_url, request)) - Response.new(response[:status] == '1', response[:reason], response, + Response.new( + response[:status] == '1', + response[:reason], + response, test: test?, - authorization: "#{response[:datacash_reference]};#{response[:authcode]};#{response[:ca_reference]}") + authorization: "#{response[:datacash_reference]};#{response[:authcode]};#{response[:ca_reference]}" + ) end def format_date(month, year) diff --git a/lib/active_merchant/billing/gateways/decidir.rb b/lib/active_merchant/billing/gateways/decidir.rb index 9f44a6a8d6a..e06ce8da3fb 100644 --- a/lib/active_merchant/billing/gateways/decidir.rb +++ b/lib/active_merchant/billing/gateways/decidir.rb @@ -83,6 +83,11 @@ def void(authorization, options = {}) commit(:post, "payments/#{authorization}/refunds", post) end + def inquire(authorization, options = {}) + options[:action] = 'inquire' + commit(:get, "payments/#{authorization}", nil, options) + end + def verify(credit_card, options = {}) raise ArgumentError, 'Verify is not supported on Decidir gateways unless the preauth_mode option is enabled' unless @options[:preauth_mode] @@ -162,29 +167,55 @@ def add_amount(post, money, options) post[:amount] = localized_amount(money, currency).to_i end - def add_payment(post, credit_card, options) - card_data = {} + def add_payment(post, payment_method, options) + add_common_payment_data(post, payment_method, options) + + case payment_method + when NetworkTokenizationCreditCard + add_network_token(post, payment_method, options) + else + add_credit_card(post, payment_method, options) + end + end + + def add_common_payment_data(post, payment_method, options) + post[:card_data] = {} + + data = post[:card_data] + data[:card_holder_identification] = {} + data[:card_holder_identification][:type] = options[:card_holder_identification_type] if options[:card_holder_identification_type] + data[:card_holder_identification][:number] = options[:card_holder_identification_number] if options[:card_holder_identification_number] + data[:card_holder_name] = payment_method.name if payment_method.name + + # additional data used for Visa transactions + data[:card_holder_door_number] = options[:card_holder_door_number].to_i if options[:card_holder_door_number] + data[:card_holder_birthday] = options[:card_holder_birthday] if options[:card_holder_birthday] + end + + def add_network_token(post, payment_method, options) + post[:is_tokenized_payment] = true + post[:fraud_detection] ||= {} + post[:fraud_detection][:sent_to_cs] = false + post[:card_data][:last_four_digits] = options[:last_4] + + post[:token_card_data] = { + token: payment_method.number, + eci: payment_method.eci, + cryptogram: payment_method.payment_cryptogram + } + end + + def add_credit_card(post, credit_card, options) + card_data = post[:card_data] card_data[:card_number] = credit_card.number card_data[:card_expiration_month] = format(credit_card.month, :two_digits) card_data[:card_expiration_year] = format(credit_card.year, :two_digits) card_data[:security_code] = credit_card.verification_value if credit_card.verification_value? - card_data[:card_holder_name] = credit_card.name if credit_card.name # the device_unique_id has to be sent in via the card data (as device_unique_identifier) no other fraud detection fields require this - if options[:fraud_detection].present? - card_data[:fraud_detection] = {} if (options[:fraud_detection][:device_unique_id]).present? - card_data[:fraud_detection][:device_unique_identifier] = (options[:fraud_detection][:device_unique_id]) if (options[:fraud_detection][:device_unique_id]).present? + if (device_id = options.dig(:fraud_detection, :device_unique_id)) + card_data[:fraud_detection] = { device_unique_identifier: device_id } end - - # additional data used for Visa transactions - card_data[:card_holder_door_number] = options[:card_holder_door_number].to_i if options[:card_holder_door_number] - card_data[:card_holder_birthday] = options[:card_holder_birthday] if options[:card_holder_birthday] - - card_data[:card_holder_identification] = {} - card_data[:card_holder_identification][:type] = options[:card_holder_identification_type] if options[:card_holder_identification_type] - card_data[:card_holder_identification][:number] = options[:card_holder_identification_number] if options[:card_holder_identification_number] - - post[:card_data] = card_data end def add_aggregate_data(post, options) @@ -267,7 +298,7 @@ def commit(method, endpoint, parameters, options = {}) response = parse(raw_response) end - success = success_from(response) + success = success_from(response, options) Response.new( success, message_from(success, response), @@ -279,7 +310,7 @@ def commit(method, endpoint, parameters, options = {}) end def post_data(parameters = {}) - parameters.to_json + parameters&.to_json end def parse(body) @@ -311,8 +342,14 @@ def message_from(success, response) message end - def success_from(response) - response['status'] == 'approved' || response['status'] == 'pre_approved' + def success_from(response, options) + status = %w(approved pre_approved) + + if options[:action] == 'inquire' + status.include?(response['status']) || response['status'] == 'rejected' + else + status.include?(response['status']) + end end def authorization_from(response) diff --git a/lib/active_merchant/billing/gateways/deepstack.rb b/lib/active_merchant/billing/gateways/deepstack.rb new file mode 100644 index 00000000000..796f3d601c2 --- /dev/null +++ b/lib/active_merchant/billing/gateways/deepstack.rb @@ -0,0 +1,382 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class DeepstackGateway < Gateway + self.test_url = 'https://api.sandbox.deepstack.io' + self.live_url = 'https://api.deepstack.io' + + self.supported_countries = ['US'] + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover] + self.money_format = :cents + + self.homepage_url = 'https://deepstack.io/' + self.display_name = 'Deepstack Gateway' + + STANDARD_ERROR_CODE_MAPPING = {} + + def initialize(options = {}) + requires!(options, :publishable_api_key, :app_id, :shared_secret) + @publishable_api_key, @app_id, @shared_secret = options.values_at(:publishable_api_key, :app_id, :shared_secret) + super + end + + def purchase(money, payment, options = {}) + post = {} + add_payment(post, payment, options) + add_order(post, money, options) + add_purchase_capture(post) + add_address(post, payment, options) + add_customer_data(post, options) + commit('sale', post) + end + + def authorize(money, payment, options = {}) + post = {} + add_payment(post, payment, options) + add_order(post, money, options) + add_address(post, payment, options) + add_customer_data(post, options) + + commit('auth', post) + end + + def capture(money, authorization, options = {}) + post = {} + add_invoice(post, money, authorization, options) + + commit('capture', post) + end + + def refund(money, authorization, options = {}) + post = {} + add_invoice(post, money, authorization, options) + commit('refund', post) + end + + def void(money, authorization, options = {}) + post = {} + add_invoice(post, money, authorization, options) + commit('void', post) + end + + def verify(credit_card, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, credit_card, options) } + r.process(:ignore_result) { void(0, r.authorization, options) } + end + end + + def get_token(credit_card, options = {}) + post = {} + add_payment_instrument(post, credit_card, options) + add_address_payment_instrument(post, credit_card, options) + commit('gettoken', post) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Bearer )\w+), '\1[FILTERED]'). + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r((Hmac: )[\w=]+), '\1[FILTERED]'). + gsub(%r((\\"account_number\\":\\")[\w*]+), '\1[FILTERED]'). + gsub(%r((\\"cvv\\":\\")\w+), '\1[FILTERED]'). + gsub(%r((\\"expiration\\":\\")\w+), '\1[FILTERED]') + end + + private + + def add_customer_data(post, options) + post[:meta] ||= {} + + add_shipping(post, options) if options.key?(:shipping_address) + post[:meta][:client_customer_id] = options[:customer] if options[:customer] + post[:meta][:client_transaction_id] = options[:order_id] if options[:order_id] + post[:meta][:client_transaction_description] = options[:description] if options[:description] + post[:meta][:client_invoice_id] = options[:invoice] if options[:invoice] + post[:meta][:card_holder_ip_address] = options[:ip] if options[:ip] + end + + def add_address(post, creditcard, options) + return post unless options.key?(:address) || options.key?(:billing_address) + + billing_address = options[:address] || options[:billing_address] + post[:source] ||= {} + + post[:source][:billing_contact] = {} + post[:source][:billing_contact][:first_name] = billing_address[:first_name] if billing_address[:first_name] + post[:source][:billing_contact][:last_name] = billing_address[:last_name] if billing_address[:last_name] + post[:source][:billing_contact][:phone] = billing_address[:phone] if billing_address[:phone] + post[:source][:billing_contact][:email] = options[:email] if options[:email] + post[:source][:billing_contact][:address] = {} + post[:source][:billing_contact][:address][:line_1] = billing_address[:address1] if billing_address[:address1] + post[:source][:billing_contact][:address][:line_2] = billing_address[:address2] if billing_address[:address2] + post[:source][:billing_contact][:address][:city] = billing_address[:city] if billing_address[:city] + post[:source][:billing_contact][:address][:state] = billing_address[:state] if billing_address[:state] + post[:source][:billing_contact][:address][:postal_code] = billing_address[:zip] if billing_address[:zip] + post[:source][:billing_contact][:address][:country_code] = billing_address[:country] if billing_address[:country] + end + + def add_address_payment_instrument(post, creditcard, options) + return post unless options.key?(:address) || options.key?(:billing_address) + + billing_address = options[:address] || options[:billing_address] + post[:source] = {} unless post.key?(:payment_instrument) + + post[:payment_instrument][:billing_contact] = {} + post[:payment_instrument][:billing_contact][:first_name] = billing_address[:first_name] if billing_address[:first_name] + post[:payment_instrument][:billing_contact][:last_name] = billing_address[:last_name] if billing_address[:last_name] + post[:payment_instrument][:billing_contact][:phone] = billing_address[:phone] if billing_address[:phone] + post[:payment_instrument][:billing_contact][:email] = billing_address[:email] if billing_address[:email] + post[:payment_instrument][:billing_contact][:address] = {} + post[:payment_instrument][:billing_contact][:address][:line_1] = billing_address[:address1] if billing_address[:address1] + post[:payment_instrument][:billing_contact][:address][:line_2] = billing_address[:address2] if billing_address[:address2] + post[:payment_instrument][:billing_contact][:address][:city] = billing_address[:city] if billing_address[:city] + post[:payment_instrument][:billing_contact][:address][:state] = billing_address[:state] if billing_address[:state] + post[:payment_instrument][:billing_contact][:address][:postal_code] = billing_address[:zip] if billing_address[:zip] + post[:payment_instrument][:billing_contact][:address][:country_code] = billing_address[:country] if billing_address[:country] + end + + def add_shipping(post, options = {}) + return post unless options.key?(:shipping_address) + + shipping = options[:shipping_address] + post[:meta][:shipping_info] = {} + post[:meta][:shipping_info][:first_name] = shipping[:first_name] if shipping[:first_name] + post[:meta][:shipping_info][:last_name] = shipping[:last_name] if shipping[:last_name] + post[:meta][:shipping_info][:phone] = shipping[:phone] if shipping[:phone] + post[:meta][:shipping_info][:email] = shipping[:email] if shipping[:email] + post[:meta][:shipping_info][:address] = {} + post[:meta][:shipping_info][:address][:line_1] = shipping[:address1] if shipping[:address1] + post[:meta][:shipping_info][:address][:line_2] = shipping[:address2] if shipping[:address2] + post[:meta][:shipping_info][:address][:city] = shipping[:city] if shipping[:city] + post[:meta][:shipping_info][:address][:state] = shipping[:state] if shipping[:state] + post[:meta][:shipping_info][:address][:postal_code] = shipping[:zip] if shipping[:zip] + post[:meta][:shipping_info][:address][:country_code] = shipping[:country] if shipping[:country] + end + + def add_invoice(post, money, authorization, options) + post[:amount] = amount(money) + post[:charge] = authorization + end + + def add_payment(post, payment, options) + if payment.kind_of?(String) + post[:source] = {} + post[:source][:type] = 'card_on_file' + post[:source][:card_on_file] = {} + post[:source][:card_on_file][:id] = payment + post[:source][:card_on_file][:cvv] = options[:verification_value] || '' + post[:source][:card_on_file][:customer_id] = options[:customer_id] || '' + # credit card object + elsif payment.respond_to?(:number) + post[:source] = {} + post[:source][:type] = 'credit_card' + post[:source][:credit_card] = {} + post[:source][:credit_card][:account_number] = payment.number + post[:source][:credit_card][:cvv] = payment.verification_value || '' + post[:source][:credit_card][:expiration] = '%02d%02d' % [payment.month, payment.year % 100] + post[:source][:credit_card][:customer_id] = options[:customer_id] || '' + end + end + + def add_payment_instrument(post, creditcard, options) + if creditcard.kind_of?(String) + post[:source] = creditcard + return post + end + return post unless creditcard.respond_to?(:number) + + post[:payment_instrument] = {} + post[:payment_instrument][:type] = 'credit_card' + post[:payment_instrument][:credit_card] = {} + post[:payment_instrument][:credit_card][:account_number] = creditcard.number + post[:payment_instrument][:credit_card][:expiration] = '%02d%02d' % [creditcard.month, creditcard.year % 100] + post[:payment_instrument][:credit_card][:cvv] = creditcard.verification_value + end + + def add_order(post, amount, options) + post[:transaction] ||= {} + + post[:transaction][:amount] = amount + post[:transaction][:cof_type] = options.key?(:cof_type) ? options[:cof_type].upcase : 'UNSCHEDULED_CARDHOLDER' + post[:transaction][:capture] = false # Change this in the request (auth/charge) + post[:transaction][:currency_code] = (options[:currency] || currency(amount).upcase) + post[:transaction][:avs] = options[:avs] || true # default avs to true unless told otherwise + post[:transaction][:save_payment_instrument] = options[:save_payment_instrument] || false + end + + def add_purchase_capture(post) + post[:transaction] ||= {} + post[:transaction][:capture] = true + end + + def parse(body) + return {} if !body || body.empty? + + JSON.parse(body) + end + + def commit(action, parameters, method = 'POST') + url = (test? ? test_url : live_url) + if no_hmac(action) + request_headers = headers.merge(create_basic(parameters, action)) + else + request_headers = headers.merge(create_hmac(parameters, method)) + end + request_url = url + get_url(action) + begin + response = parse(ssl_post(request_url, post_data(action, parameters), request_headers)) + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + avs_result: AVSResult.new(code: response['avs_result']), + cvv_result: CVVResult.new(response['cvv_result']), + test: test?, + error_code: error_code_from(response) + ) + rescue ResponseError => e + Response.new( + false, + message_from_error(e.response.body), + response_error(e.response.body) + ) + rescue JSON::ParserError + Response.new( + false, + message_from(response), + json_error(response) + ) + end + end + + def headers + { + 'Accept' => 'text/plain', + 'Content-Type' => 'application/json' + } + end + + def response_error(response) + parse(response) + rescue JSON::ParserError + json_error(response) + end + + def json_error(response) + msg = 'Invalid response received from the Conekta API.' + msg += " (The raw response returned by the API was #{response.inspect})" + { + 'message' => msg + } + end + + def success_from(response) + success = false + if response.key?('response_code') + success = response['response_code'] == '00' + # Hack because token/payment instrument methods do not return a response_code + elsif response.key?('id') + success = true if response['id'].start_with?('tok', 'card') + end + + return success + end + + def message_from(response) + response = JSON.parse(response) if response.is_a?(String) + if response.key?('message') + return response['message'] + elsif response.key?('detail') + return response['detail'] + end + end + + def message_from_error(response) + if response.is_a?(String) + response.gsub!('\\"', '"') + response = JSON.parse(response) + end + + if response.key?('detail') + return response['detail'] + elsif response.key?('message') + return response['message'] + end + end + + def authorization_from(response) + response['id'] + end + + def post_data(action, parameters = {}) + return JSON.generate(parameters) + end + + def error_code_from(response) + error_code = nil + error_code = response['response_code'] unless success_from(response) + if error = response.dig('detail') + error_code = error + elsif error = response.dig('error') + error_code = error.dig('reason', 'id') + end + error_code + end + + def get_url(action) + base = '/api/v1/' + case action + when 'sale' + return base + 'payments/charge' + when 'auth' + return base + 'payments/charge' + when 'capture' + return base + 'payments/capture' + when 'void' + return base + 'payments/refund' + when 'refund' + return base + 'payments/refund' + when 'gettoken' + return base + 'vault/token' + when 'vault' + return base + 'vault/payment-instrument/token' + else + return base + 'noaction' + end + end + + def no_hmac(action) + case action + when 'gettoken' + return true + else + return false + end + end + + def create_basic(post, method) + return { 'Authorization' => "Bearer #{@publishable_api_key}" } + end + + def create_hmac(post, method) + # Need requestDate, requestMethod, Nonce, AppIDKey + app_id_key = @app_id + request_method = method.upcase + uuid = SecureRandom.uuid + request_time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ') + + string_to_hash = "#{app_id_key}|#{request_method}|#{request_time}|#{uuid}|#{JSON.generate(post)}" + signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), Base64.strict_decode64(@shared_secret), string_to_hash) + base64_signature = Base64.strict_encode64(signature) + hmac_header = Base64.strict_encode64("#{app_id_key}|#{request_method}|#{request_time}|#{uuid}|#{base64_signature}") + return { 'hmac' => hmac_header } + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/ebanx.rb b/lib/active_merchant/billing/gateways/ebanx.rb index 82f5353c998..4588eddb7f7 100644 --- a/lib/active_merchant/billing/gateways/ebanx.rb +++ b/lib/active_merchant/billing/gateways/ebanx.rb @@ -4,20 +4,14 @@ class EbanxGateway < Gateway self.test_url = 'https://sandbox.ebanxpay.com/ws/' self.live_url = 'https://api.ebanxpay.com/ws/' - self.supported_countries = %w(BR MX CO CL AR PE) + self.supported_countries = %w(BR MX CO CL AR PE BO EC) self.default_currency = 'USD' - self.supported_cardtypes = %i[visa master american_express discover diners_club] + self.supported_cardtypes = %i[visa master american_express discover diners_club elo hipercard] self.homepage_url = 'http://www.ebanx.com/' self.display_name = 'EBANX' - CARD_BRAND = { - visa: 'visa', - master: 'master_card', - american_express: 'amex', - discover: 'discover', - diners_club: 'diners' - } + TAGS = ['Spreedly'] URL_MAP = { purchase: 'direct', @@ -25,7 +19,9 @@ class EbanxGateway < Gateway capture: 'capture', refund: 'refund', void: 'cancel', - store: 'token' + store: 'token', + inquire: 'query', + verify: 'verifycard' } HTTP_METHOD = { @@ -34,16 +30,9 @@ class EbanxGateway < Gateway capture: :get, refund: :post, void: :get, - store: :post - } - - VERIFY_AMOUNT_PER_COUNTRY = { - 'br' => 100, - 'ar' => 100, - 'co' => 100, - 'pe' => 300, - 'mx' => 2000, - 'cl' => 80000 + store: :post, + inquire: :get, + verify: :post } def initialize(options = {}) @@ -57,7 +46,7 @@ def purchase(money, payment, options = {}) add_operation(post) add_invoice(post, money, options) add_customer_data(post, payment, options) - add_card_or_token(post, payment) + add_card_or_token(post, payment, options) add_address(post, options) add_customer_responsible_person(post, payment, options) add_additional_data(post, options) @@ -71,7 +60,7 @@ def authorize(money, payment, options = {}) add_operation(post) add_invoice(post, money, options) add_customer_data(post, payment, options) - add_card_or_token(post, payment) + add_card_or_token(post, payment, options) add_address(post, options) add_customer_responsible_person(post, payment, options) add_additional_data(post, options) @@ -111,17 +100,30 @@ def void(authorization, options = {}) def store(credit_card, options = {}) post = {} add_integration_key(post) - add_payment_details(post, credit_card) - post[:country] = customer_country(options) + customer_country(post, options) + add_payment_type(post) + post[:creditcard] = payment_details(credit_card) commit(:store, post) end def verify(credit_card, options = {}) - MultiResponse.run(:use_first_response) do |r| - r.process { authorize(VERIFY_AMOUNT_PER_COUNTRY[customer_country(options)], credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options) } - end + post = {} + add_integration_key(post) + add_payment_type(post) + customer_country(post, options) + post[:card] = payment_details(credit_card) + post[:device_id] = options[:device_id] if options[:device_id] + + commit(:verify, post) + end + + def inquire(authorization, options = {}) + post = {} + add_integration_key(post) + add_authorization(post, authorization) + + commit(:inquire, post) end def supports_scrubbing? @@ -151,7 +153,7 @@ def add_authorization(post, authorization) def add_customer_data(post, payment, options) post[:payment][:name] = customer_name(payment, options) - post[:payment][:email] = options[:email] || 'unspecified@example.com' + post[:payment][:email] = options[:email] post[:payment][:document] = options[:document] post[:payment][:birth_date] = options[:birth_date] if options[:birth_date] end @@ -186,15 +188,15 @@ def add_invoice(post, money, options) post[:payment][:order_number] = options[:order_id][0..39] if options[:order_id] end - def add_card_or_token(post, payment) - payment, brand = payment.split('|') if payment.is_a?(String) - post[:payment][:payment_type_code] = payment.is_a?(String) ? brand : CARD_BRAND[payment.brand.to_sym] + def add_card_or_token(post, payment, options) + payment = payment.split('|')[0] if payment.is_a?(String) + add_payment_type(post[:payment]) post[:payment][:creditcard] = payment_details(payment) + post[:payment][:creditcard][:soft_descriptor] = options[:soft_descriptor] if options[:soft_descriptor] end - def add_payment_details(post, payment) - post[:payment_type_code] = CARD_BRAND[payment.brand.to_sym] - post[:creditcard] = payment_details(payment) + def add_payment_type(post) + post[:payment_type_code] = 'creditcard' end def payment_details(payment) @@ -216,6 +218,7 @@ def add_additional_data(post, options) post[:metadata] = {} if post[:metadata].nil? post[:metadata][:merchant_payment_code] = options[:order_id] if options[:order_id] post[:processing_type] = options[:processing_type] if options[:processing_type] + post[:payment][:tags] = TAGS end def parse(body) @@ -231,7 +234,7 @@ def commit(action, parameters) Response.new( success, - message_from(response), + message_from(action, response), response, authorization: authorization_from(action, parameters, response), test: test?, @@ -253,28 +256,41 @@ def add_processing_type_to_commit_headers(commit_headers, processing_type) end def success_from(action, response) - if %i[purchase capture refund].include?(action) - response.try(:[], 'payment').try(:[], 'status') == 'CO' - elsif action == :authorize - response.try(:[], 'payment').try(:[], 'status') == 'PE' - elsif action == :void - response.try(:[], 'payment').try(:[], 'status') == 'CA' - elsif action == :store - response.try(:[], 'status') == 'SUCCESS' + status = response.dig('payment', 'status') + + case action + when :purchase, :capture, :refund + status == 'CO' + when :authorize + status == 'PE' + when :void + status == 'CA' + when :verify + response.dig('card_verification', 'transaction_status', 'code') == 'OK' + when :store, :inquire + response.dig('status') == 'SUCCESS' else false end end - def message_from(response) + def message_from(action, response) return response['status_message'] if response['status'] == 'ERROR' - response.try(:[], 'payment').try(:[], 'transaction_status').try(:[], 'description') + if action == :verify + response.dig('card_verification', 'transaction_status', 'description') + else + response.dig('payment', 'transaction_status', 'description') + end end def authorization_from(action, parameters, response) if action == :store - "#{response.try(:[], 'token')}|#{CARD_BRAND[parameters[:payment_type_code].to_sym]}" + if success_from(action, response) + "#{response.try(:[], 'token')}|#{response['payment_type_code']}" + else + response.try(:[], 'token') + end else response.try(:[], 'payment').try(:[], 'hash') end @@ -294,7 +310,7 @@ def url_for(hostname, action, parameters) end def requires_http_get(action) - return true if %i[capture void].include?(action) + return true if %i[capture void inquire].include?(action) false end @@ -315,9 +331,9 @@ def error_code_from(response, success) end end - def customer_country(options) + def customer_country(post, options) if country = options[:country] || (options[:billing_address][:country] if options[:billing_address]) - country.downcase + post[:country] = country.downcase end end diff --git a/lib/active_merchant/billing/gateways/efsnet.rb b/lib/active_merchant/billing/gateways/efsnet.rb index 00a5579c61f..d3ec02270ce 100644 --- a/lib/active_merchant/billing/gateways/efsnet.rb +++ b/lib/active_merchant/billing/gateways/efsnet.rb @@ -145,11 +145,15 @@ def add_creditcard(post, creditcard) def commit(action, parameters) response = parse(ssl_post(test? ? self.test_url : self.live_url, post_data(action, parameters), 'Content-Type' => 'text/xml')) - Response.new(success?(response), message_from(response[:result_message]), response, + Response.new( + success?(response), + message_from(response[:result_message]), + response, test: test?, authorization: authorization_from(response, parameters), avs_result: { code: response[:avs_response_code] }, - cvv_result: response[:cvv_response_code]) + cvv_result: response[:cvv_response_code] + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/element.rb b/lib/active_merchant/billing/gateways/element.rb index 527a6306d7d..0385db547d7 100644 --- a/lib/active_merchant/billing/gateways/element.rb +++ b/lib/active_merchant/billing/gateways/element.rb @@ -17,6 +17,11 @@ class ElementGateway < Gateway SERVICE_TEST_URL = 'https://certservices.elementexpress.com/express.asmx' SERVICE_LIVE_URL = 'https://services.elementexpress.com/express.asmx' + NETWORK_TOKEN_TYPE = { + apple_pay: '2', + google_pay: '1' + } + def initialize(options = {}) requires!(options, :account_id, :account_token, :application_id, :acceptor_id, :application_name, :application_version) super @@ -171,6 +176,8 @@ def add_payment_method(xml, payment) add_payment_account_id(xml, payment) elsif payment.is_a?(Check) add_echeck(xml, payment) + elsif payment.is_a?(NetworkTokenizationCreditCard) + add_network_tokenization_card(xml, payment) else add_credit_card(xml, payment) end @@ -200,7 +207,7 @@ def add_transaction(xml, money, options = {}) xml.TransactionID options[:trans_id] if options[:trans_id] xml.TransactionAmount amount(money.to_i) if money xml.MarketCode market_code(money, options) if options[:market_code] || money - xml.ReferenceNumber options[:order_id] || SecureRandom.hex(20) + xml.ReferenceNumber options[:order_id].present? ? options[:order_id][0, 50] : SecureRandom.hex(20) xml.TicketNumber options[:ticket_number] if options[:ticket_number] xml.MerchantSuppliedTransactionId options[:merchant_supplied_transaction_id] if options[:merchant_supplied_transaction_id] xml.PaymentType options[:payment_type] if options[:payment_type] @@ -233,7 +240,7 @@ def add_credit_card(xml, payment) xml.CardNumber payment.number xml.ExpirationMonth format(payment.month, :two_digits) xml.ExpirationYear format(payment.year, :two_digits) - xml.CardholderName payment.first_name + ' ' + payment.last_name + xml.CardholderName "#{payment.first_name} #{payment.last_name}" xml.CVV payment.verification_value end end @@ -246,8 +253,21 @@ def add_echeck(xml, payment) end end + def add_network_tokenization_card(xml, payment) + xml.card do + xml.CardNumber payment.number + xml.ExpirationMonth format(payment.month, :two_digits) + xml.ExpirationYear format(payment.year, :two_digits) + xml.CardholderName "#{payment.first_name} #{payment.last_name}" + xml.Cryptogram payment.payment_cryptogram + xml.ElectronicCommerceIndicator payment.eci if payment.eci.present? + xml.WalletType NETWORK_TOKEN_TYPE.fetch(payment.source, '0') + end + end + def add_address(xml, options) if address = options[:billing_address] || options[:address] + address[:email] ||= options[:email] xml.address do xml.BillingAddress1 address[:address1] if address[:address1] xml.BillingAddress2 address[:address2] if address[:address2] @@ -343,7 +363,6 @@ def build_soap_request xml['soap'].Envelope('xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/') do - xml['soap'].Body do yield(xml) end diff --git a/lib/active_merchant/billing/gateways/epay.rb b/lib/active_merchant/billing/gateways/epay.rb index d26382cfe3c..83c35088833 100644 --- a/lib/active_merchant/billing/gateways/epay.rb +++ b/lib/active_merchant/billing/gateways/epay.rb @@ -174,17 +174,21 @@ def commit(action, params) response = send("do_#{action}", params) if action == :authorize - Response.new response['accept'].to_i == 1, + Response.new( + response['accept'].to_i == 1, response['errortext'], response, test: test?, authorization: response['tid'] + ) else - Response.new response['result'] == 'true', + Response.new( + response['result'] == 'true', messages(response['epay'], response['pbs']), response, test: test?, authorization: params[:transaction] + ) end end diff --git a/lib/active_merchant/billing/gateways/evo_ca.rb b/lib/active_merchant/billing/gateways/evo_ca.rb index 8aeabf72977..cd9848eb2a7 100644 --- a/lib/active_merchant/billing/gateways/evo_ca.rb +++ b/lib/active_merchant/billing/gateways/evo_ca.rb @@ -279,11 +279,15 @@ def commit(action, money, parameters) response = parse(data) message = message_from(response) - Response.new(success?(response), message, response, + Response.new( + success?(response), + message, + response, test: test?, authorization: response['transactionid'], avs_result: { code: response['avsresponse'] }, - cvv_result: response['cvvresponse']) + cvv_result: response['cvvresponse'] + ) end def message_from(response) diff --git a/lib/active_merchant/billing/gateways/eway.rb b/lib/active_merchant/billing/gateways/eway.rb index 3032bb2d4fe..c6e21c658af 100644 --- a/lib/active_merchant/billing/gateways/eway.rb +++ b/lib/active_merchant/billing/gateways/eway.rb @@ -111,11 +111,13 @@ def commit(url, money, parameters) raw_response = ssl_post(url, post_data(parameters)) response = parse(raw_response) - Response.new(success?(response), + Response.new( + success?(response), message_from(response[:ewaytrxnerror]), response, authorization: response[:ewaytrxnnumber], - test: test?) + test: test? + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/eway_managed.rb b/lib/active_merchant/billing/gateways/eway_managed.rb index 02cd5b5cf6f..c65ad5206b0 100644 --- a/lib/active_merchant/billing/gateways/eway_managed.rb +++ b/lib/active_merchant/billing/gateways/eway_managed.rb @@ -222,9 +222,13 @@ def commit(action, post) end response = parse(raw) - EwayResponse.new(response[:success], response[:message], response, + EwayResponse.new( + response[:success], + response[:message], + response, test: test?, - authorization: response[:auth_code]) + authorization: response[:auth_code] + ) end # Where we build the full SOAP 1.2 request using builder diff --git a/lib/active_merchant/billing/gateways/exact.rb b/lib/active_merchant/billing/gateways/exact.rb index 144e3dc1359..6b99cd66e2a 100644 --- a/lib/active_merchant/billing/gateways/exact.rb +++ b/lib/active_merchant/billing/gateways/exact.rb @@ -158,11 +158,15 @@ def expdate(credit_card) def commit(action, request) response = parse(ssl_post(self.live_url, build_request(action, request), POST_HEADERS)) - Response.new(successful?(response), message_from(response), response, + Response.new( + successful?(response), + message_from(response), + response, test: test?, authorization: authorization_from(response), avs_result: { code: response[:avs] }, - cvv_result: response[:cvv2]) + cvv_result: response[:cvv2] + ) rescue ResponseError => e case e.response.code when '401' diff --git a/lib/active_merchant/billing/gateways/federated_canada.rb b/lib/active_merchant/billing/gateways/federated_canada.rb index 9399db829f5..43460286317 100644 --- a/lib/active_merchant/billing/gateways/federated_canada.rb +++ b/lib/active_merchant/billing/gateways/federated_canada.rb @@ -121,11 +121,15 @@ def commit(action, money, parameters) response = parse(data) message = message_from(response) - Response.new(success?(response), message, response, + Response.new( + success?(response), + message, + response, test: test?, authorization: response['transactionid'], avs_result: { code: response['avsresponse'] }, - cvv_result: response['cvvresponse']) + cvv_result: response['cvvresponse'] + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/firstdata_e4.rb b/lib/active_merchant/billing/gateways/firstdata_e4.rb index aa2cb1e39c8..35191eeee72 100755 --- a/lib/active_merchant/billing/gateways/firstdata_e4.rb +++ b/lib/active_merchant/billing/gateways/firstdata_e4.rb @@ -354,12 +354,16 @@ def commit(action, request, credit_card = nil) response = parse_error(e.response) end - Response.new(successful?(response), message_from(response), response, + Response.new( + successful?(response), + message_from(response), + response, test: test?, authorization: successful?(response) ? response_authorization(action, response, credit_card) : '', avs_result: { code: response[:avs] }, cvv_result: response[:cvv2], - error_code: standard_error_code(response)) + error_code: standard_error_code(response) + ) end def successful?(response) diff --git a/lib/active_merchant/billing/gateways/firstdata_e4_v27.rb b/lib/active_merchant/billing/gateways/firstdata_e4_v27.rb index e6c438916d5..7ac0901d891 100644 --- a/lib/active_merchant/billing/gateways/firstdata_e4_v27.rb +++ b/lib/active_merchant/billing/gateways/firstdata_e4_v27.rb @@ -380,12 +380,16 @@ def commit(action, data, credit_card = nil) response = parse_error(e.response) end - Response.new(successful?(response), message_from(response), response, + Response.new( + successful?(response), + message_from(response), + response, test: test?, authorization: successful?(response) ? response_authorization(action, response, credit_card) : '', avs_result: { code: response[:avs] }, cvv_result: response[:cvv2], - error_code: standard_error_code(response)) + error_code: standard_error_code(response) + ) end def headers(method, url, request) diff --git a/lib/active_merchant/billing/gateways/garanti.rb b/lib/active_merchant/billing/gateways/garanti.rb index 2bdd1071981..57a78d4104e 100644 --- a/lib/active_merchant/billing/gateways/garanti.rb +++ b/lib/active_merchant/billing/gateways/garanti.rb @@ -219,11 +219,13 @@ def commit(money, request) success = success?(response) - Response.new(success, + Response.new( + success, success ? 'Approved' : "Declined (Reason: #{response[:reason_code]} - #{response[:error_msg]} - #{response[:sys_err_msg]})", response, test: test?, - authorization: response[:order_id]) + authorization: response[:order_id] + ) end def parse(body) diff --git a/lib/active_merchant/billing/gateways/global_collect.rb b/lib/active_merchant/billing/gateways/global_collect.rb index 0101abfc84b..52b5409193e 100644 --- a/lib/active_merchant/billing/gateways/global_collect.rb +++ b/lib/active_merchant/billing/gateways/global_collect.rb @@ -2,6 +2,8 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: class GlobalCollectGateway < Gateway class_attribute :preproduction_url + class_attribute :ogone_direct_test + class_attribute :ogone_direct_live self.display_name = 'GlobalCollect' self.homepage_url = 'http://www.globalcollect.com/' @@ -9,6 +11,8 @@ class GlobalCollectGateway < Gateway self.test_url = 'https://eu.sandbox.api-ingenico.com' self.preproduction_url = 'https://world.preprod.api-ingenico.com' self.live_url = 'https://world.api-ingenico.com' + self.ogone_direct_test = 'https://payment.preprod.direct.worldline-solutions.com' + self.ogone_direct_live = 'https://payment.direct.worldline-solutions.com' self.supported_countries = %w[AD AE AG AI AL AM AO AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BW BY BZ CA CC CD CF CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HN HR HT HU ID IE IL IM IN IS IT JM JO JP KE KG KH KI KM KN KR KW KY KZ LA LB LC LI LK LR LS LT LU LV MA MC MD ME MF MG MH MK MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PL PN PS PT PW QA RE RO RS RU RW SA SB SC SE SG SH SI SJ SK SL SM SN SR ST SV SZ TC TD TG TH TJ TL TM TN TO TR TT TV TW TZ UA UG US UY UZ VC VE VG VI VN WF WS ZA ZM ZW] self.default_currency = 'USD' @@ -36,7 +40,7 @@ def authorize(money, payment, options = {}) add_creator_info(post, options) add_fraud_fields(post, options) add_external_cardholder_authentication_data(post, options) - commit(:authorize, post, options: options) + commit(:post, :authorize, post, options: options) end def capture(money, authorization, options = {}) @@ -44,7 +48,7 @@ def capture(money, authorization, options = {}) add_order(post, money, options, capture: true) add_customer_data(post, options) add_creator_info(post, options) - commit(:capture, post, authorization: authorization) + commit(:post, :capture, post, authorization: authorization) end def refund(money, authorization, options = {}) @@ -52,13 +56,13 @@ def refund(money, authorization, options = {}) add_amount(post, money, options) add_refund_customer_data(post, options) add_creator_info(post, options) - commit(:refund, post, authorization: authorization) + commit(:post, :refund, post, authorization: authorization) end def void(authorization, options = {}) post = nestable_hash add_creator_info(post, options) - commit(:void, post, authorization: authorization) + commit(:post, :void, post, authorization: authorization) end def verify(payment, options = {}) @@ -68,6 +72,10 @@ def verify(payment, options = {}) end end + def inquire(authorization, options = {}) + commit(:get, :inquire, nil, authorization: authorization) + end + def supports_scrubbing? true end @@ -76,7 +84,10 @@ def scrub(transcript) transcript. gsub(%r((Authorization: )[^\\]*)i, '\1[FILTERED]'). gsub(%r(("cardNumber\\+":\\+")\d+), '\1[FILTERED]'). - gsub(%r(("cvv\\+":\\+")\d+), '\1[FILTERED]') + gsub(%r(("cvv\\+":\\+")\d+), '\1[FILTERED]'). + gsub(%r(("dpan\\+":\\+")\d+), '\1[FILTERED]'). + gsub(%r(("pan\\+":\\+")\d+), '\1[FILTERED]'). + gsub(%r(("cryptogram\\+":\\+"|("cavv\\+" : \\+"))[^\\]*), '\1[FILTERED]') end private @@ -89,7 +100,9 @@ def scrub(transcript) 'jcb' => '125', 'diners_club' => '132', 'cabal' => '135', - 'naranja' => '136' + 'naranja' => '136', + 'apple_pay': '302', + 'google_pay': '320' } def add_order(post, money, options, capture: false) @@ -105,7 +118,7 @@ def add_order(post, money, options, capture: false) post['order']['references']['invoiceData'] = { 'invoiceNumber' => options[:invoice] } - add_airline_data(post, options) + add_airline_data(post, options) unless ogone_direct? add_lodging_data(post, options) add_number_of_installments(post, options) if options[:number_of_installments] end @@ -125,6 +138,7 @@ def add_airline_data(post, options) airline_data['isThirdParty'] = options[:airline_data][:is_third_party] if options[:airline_data][:is_third_party] airline_data['issueDate'] = options[:airline_data][:issue_date] if options[:airline_data][:issue_date] airline_data['merchantCustomerId'] = options[:airline_data][:merchant_customer_id] if options[:airline_data][:merchant_customer_id] + airline_data['agentNumericCode'] = options[:airline_data][:agent_numeric_code] if options[:airline_data][:agent_numeric_code] airline_data['flightLegs'] = add_flight_legs(airline_options) airline_data['passengers'] = add_passengers(airline_options) @@ -239,9 +253,10 @@ def add_creator_info(post, options) end def add_amount(post, money, options = {}) + currency_ogone = 'EUR' if ogone_direct? post['amountOfMoney'] = { 'amount' => amount(money), - 'currencyCode' => options[:currency] || currency(money) + 'currencyCode' => options[:currency] || currency_ogone || currency(money) } end @@ -250,20 +265,58 @@ def add_payment(post, payment, options) month = format(payment.month, :two_digits) expirydate = "#{month}#{year}" pre_authorization = options[:pre_authorization] ? 'PRE_AUTHORIZATION' : 'FINAL_AUTHORIZATION' - post['cardPaymentMethodSpecificInput'] = { - 'paymentProductId' => BRAND_MAP[payment.brand], - 'skipAuthentication' => 'true', # refers to 3DSecure + product_id = options[:payment_product_id] || BRAND_MAP[payment.brand] + specifics_inputs = { + 'paymentProductId' => product_id, + 'skipAuthentication' => options[:skip_authentication] || 'true', # refers to 3DSecure 'skipFraudService' => 'true', 'authorizationMode' => pre_authorization } - post['cardPaymentMethodSpecificInput']['requiresApproval'] = options[:requires_approval] unless options[:requires_approval].nil? + specifics_inputs['requiresApproval'] = options[:requires_approval] unless options[:requires_approval].nil? + if payment.is_a?(NetworkTokenizationCreditCard) + add_mobile_credit_card(post, payment, options, specifics_inputs, expirydate) + elsif payment.is_a?(CreditCard) + options[:google_pay_pan_only] ? add_mobile_credit_card(post, payment, options, specifics_inputs, expirydate) : add_credit_card(post, payment, specifics_inputs, expirydate) + end + end - post['cardPaymentMethodSpecificInput']['card'] = { - 'cvv' => payment.verification_value, - 'cardNumber' => payment.number, - 'expiryDate' => expirydate, - 'cardholderName' => payment.name - } + def add_credit_card(post, payment, specifics_inputs, expirydate) + post['cardPaymentMethodSpecificInput'] = specifics_inputs.merge({ + 'card' => { + 'cvv' => payment.verification_value, + 'cardNumber' => payment.number, + 'expiryDate' => expirydate, + 'cardholderName' => payment.name + } + }) + end + + def add_mobile_credit_card(post, payment, options, specifics_inputs, expirydate) + specifics_inputs['paymentProductId'] = options[:google_pay_pan_only] ? BRAND_MAP[:google_pay] : BRAND_MAP[payment.source] + post['mobilePaymentMethodSpecificInput'] = specifics_inputs + add_decrypted_payment_data(post, payment, options, expirydate) + end + + def add_decrypted_payment_data(post, payment, options, expirydate) + if payment.is_a?(NetworkTokenizationCreditCard) && payment.payment_cryptogram + data = { + 'cardholderName' => payment.name, + 'cryptogram' => payment.payment_cryptogram, + 'eci' => payment.eci, + 'expiryDate' => expirydate, + 'dpan' => payment.number + } + data['paymentMethod'] = 'TOKENIZED_CARD' if payment.source == :google_pay + # else case when google payment is an ONLY_PAN, doesn't have cryptogram or eci. + elsif options[:google_pay_pan_only] + data = { + 'cardholderName' => payment.name, + 'expiryDate' => expirydate, + 'pan' => payment.number, + 'paymentMethod' => 'CARD' + } + end + post['mobilePaymentMethodSpecificInput']['decryptedPaymentData'] = data if data end def add_customer_data(post, options, payment = nil) @@ -295,7 +348,8 @@ def add_address(post, creditcard, options) shipping_address = options[:shipping_address] if billing_address = options[:billing_address] || options[:address] post['order']['customer']['billingAddress'] = { - 'street' => truncate(billing_address[:address1], 50), + 'street' => truncate(split_address(billing_address[:address1])[1], 50), + 'houseNumber' => split_address(billing_address[:address1])[0], 'additionalInfo' => truncate(billing_address[:address2], 50), 'zip' => billing_address[:zip], 'city' => billing_address[:city], @@ -305,7 +359,8 @@ def add_address(post, creditcard, options) end if shipping_address post['order']['customer']['shippingAddress'] = { - 'street' => truncate(shipping_address[:address1], 50), + 'street' => truncate(split_address(shipping_address[:address1])[1], 50), + 'houseNumber' => split_address(shipping_address[:address1])[0], 'additionalInfo' => truncate(shipping_address[:address2], 50), 'zip' => shipping_address[:zip], 'city' => shipping_address[:city], @@ -330,18 +385,24 @@ def add_fraud_fields(post, options) def add_external_cardholder_authentication_data(post, options) return unless threeds_2_options = options[:three_d_secure] - authentication_data = {} - authentication_data[:acsTransactionId] = threeds_2_options[:acs_transaction_id] if threeds_2_options[:acs_transaction_id] - authentication_data[:cavv] = threeds_2_options[:cavv] if threeds_2_options[:cavv] - authentication_data[:cavvAlgorithm] = threeds_2_options[:cavv_algorithm] if threeds_2_options[:cavv_algorithm] - authentication_data[:directoryServerTransactionId] = threeds_2_options[:ds_transaction_id] if threeds_2_options[:ds_transaction_id] - authentication_data[:eci] = threeds_2_options[:eci] if threeds_2_options[:eci] - authentication_data[:threeDSecureVersion] = threeds_2_options[:version] if threeds_2_options[:version] - authentication_data[:validationResult] = threeds_2_options[:authentication_response_status] if threeds_2_options[:authentication_response_status] - authentication_data[:xid] = threeds_2_options[:xid] if threeds_2_options[:xid] + authentication_data = { + priorThreeDSecureData: { acsTransactionId: threeds_2_options[:acs_transaction_id] }.compact, + cavv: threeds_2_options[:cavv], + cavvAlgorithm: threeds_2_options[:cavv_algorithm], + directoryServerTransactionId: threeds_2_options[:ds_transaction_id], + eci: threeds_2_options[:eci], + threeDSecureVersion: threeds_2_options[:version] || options[:three_ds_version], + validationResult: threeds_2_options[:authentication_response_status], + xid: threeds_2_options[:xid], + acsTransactionId: threeds_2_options[:acs_transaction_id], + flow: threeds_2_options[:flow] + }.compact post['cardPaymentMethodSpecificInput'] ||= {} post['cardPaymentMethodSpecificInput']['threeDSecure'] ||= {} + post['cardPaymentMethodSpecificInput']['threeDSecure']['merchantFraudRate'] = threeds_2_options[:merchant_fraud_rate] + post['cardPaymentMethodSpecificInput']['threeDSecure']['exemptionRequest'] = threeds_2_options[:exemption_request] + post['cardPaymentMethodSpecificInput']['threeDSecure']['secureCorporatePayment'] = threeds_2_options[:secure_corporate_payment] post['cardPaymentMethodSpecificInput']['threeDSecure']['externalCardholderAuthenticationData'] = authentication_data unless authentication_data.empty? end @@ -355,31 +416,44 @@ def parse(body) def url(action, authorization) return preproduction_url + uri(action, authorization) if @options[:url_override].to_s == 'preproduction' + return ogone_direct_url(action, authorization) if ogone_direct? (test? ? test_url : live_url) + uri(action, authorization) end + def ogone_direct_url(action, authorization) + (test? ? ogone_direct_test : ogone_direct_live) + uri(action, authorization) + end + + def ogone_direct? + @options[:url_override].to_s == 'ogone_direct' + end + def uri(action, authorization) - uri = "/v1/#{@options[:merchant_id]}/" + version = ogone_direct? ? 'v2' : 'v1' + uri = "/#{version}/#{@options[:merchant_id]}/" case action when :authorize uri + 'payments' when :capture - uri + "payments/#{authorization}/approve" + capture_name = ogone_direct? ? 'capture' : 'approve' + uri + "payments/#{authorization}/#{capture_name}" when :refund uri + "payments/#{authorization}/refund" when :void uri + "payments/#{authorization}/cancel" + when :inquire + uri + "payments/#{authorization}" end end def idempotency_key_for_signature(options) - "x-gcs-idempotence-key:#{options[:idempotency_key]}" if options[:idempotency_key] + "x-gcs-idempotence-key:#{options[:idempotency_key]}" if options[:idempotency_key] && !ogone_direct? end - def commit(action, post, authorization: nil, options: {}) + def commit(method, action, post, authorization: nil, options: {}) begin - raw_response = ssl_post(url(action, authorization), post.to_json, headers(action, post, authorization, options)) + raw_response = ssl_request(method, url(action, authorization), post&.to_json, headers(method, action, post, authorization, options)) response = parse(raw_response) rescue ResponseError => e response = parse(e.response.body) if e.response.code.to_i >= 400 @@ -405,33 +479,33 @@ def json_error(raw_response) } end - def headers(action, post, authorization = nil, options = {}) + def headers(method, action, post, authorization = nil, options = {}) headers = { 'Content-Type' => content_type, - 'Authorization' => auth_digest(action, post, authorization, options), + 'Authorization' => auth_digest(method, action, post, authorization, options), 'Date' => date } - headers['X-GCS-Idempotence-Key'] = options[:idempotency_key] if options[:idempotency_key] + headers['X-GCS-Idempotence-Key'] = options[:idempotency_key] if options[:idempotency_key] && !ogone_direct? headers end - def auth_digest(action, post, authorization = nil, options = {}) + def auth_digest(method, action, post, authorization = nil, options = {}) data = <<~REQUEST - POST + #{method.to_s.upcase} #{content_type} #{date} #{idempotency_key_for_signature(options)} #{uri(action, authorization)} REQUEST data = data.each_line.reject { |line| line.strip == '' }.join - digest = OpenSSL::Digest.new('sha256') + digest = OpenSSL::Digest.new('SHA256') key = @options[:secret_api_key] - "GCS v1HMAC:#{@options[:api_key_id]}:#{Base64.strict_encode64(OpenSSL::HMAC.digest(digest, key, data))}" + "GCS v1HMAC:#{@options[:api_key_id]}:#{Base64.strict_encode64(OpenSSL::HMAC.digest(digest, key, data)).strip}" end def date - @date ||= Time.now.strftime('%a, %d %b %Y %H:%M:%S %Z') # Must be same in digest and HTTP header + @date ||= Time.now.gmtime.strftime('%a, %d %b %Y %H:%M:%S GMT') end def content_type @@ -441,6 +515,8 @@ def content_type def success_from(action, response) return false if response['errorId'] || response['error_message'] + return %w(CAPTURED CAPTURE_REQUESTED).include?(response.dig('payment', 'status')) if response.dig('payment', 'paymentOutput', 'paymentMethod') == 'mobile' + case action when :authorize response.dig('payment', 'statusOutput', 'isAuthorized') diff --git a/lib/active_merchant/billing/gateways/iats_payments.rb b/lib/active_merchant/billing/gateways/iats_payments.rb index b8e6303f57d..ee758ea55fd 100644 --- a/lib/active_merchant/billing/gateways/iats_payments.rb +++ b/lib/active_merchant/billing/gateways/iats_payments.rb @@ -185,8 +185,13 @@ def creditcard_brand(brand) end def commit(action, parameters) - response = parse(ssl_post(url(action), post_data(action, parameters), - { 'Content-Type' => 'application/soap+xml; charset=utf-8' })) + response = parse( + ssl_post( + url(action), + post_data(action, parameters), + { 'Content-Type' => 'application/soap+xml; charset=utf-8' } + ) + ) Response.new( success_from(response), diff --git a/lib/active_merchant/billing/gateways/inspire.rb b/lib/active_merchant/billing/gateways/inspire.rb index 61f6f8c4b85..742ced15d0b 100644 --- a/lib/active_merchant/billing/gateways/inspire.rb +++ b/lib/active_merchant/billing/gateways/inspire.rb @@ -172,11 +172,14 @@ def commit(action, money, parameters) response = parse(ssl_post(self.live_url, post_data(action, parameters))) - Response.new(response['response'] == '1', message_from(response), response, + Response.new( + response['response'] == '1', + message_from(response), response, authorization: response['transactionid'], test: test?, cvv_result: response['cvvresponse'], - avs_result: { code: response['avsresponse'] }) + avs_result: { code: response['avsresponse'] } + ) end def message_from(response) diff --git a/lib/active_merchant/billing/gateways/instapay.rb b/lib/active_merchant/billing/gateways/instapay.rb index 4ca11852dd8..8045c169ad8 100644 --- a/lib/active_merchant/billing/gateways/instapay.rb +++ b/lib/active_merchant/billing/gateways/instapay.rb @@ -140,10 +140,14 @@ def commit(action, parameters) data = ssl_post self.live_url, post_data(action, parameters) response = parse(data) - Response.new(response[:success], response[:message], response, + Response.new( + response[:success], + response[:message], + response, authorization: response[:transaction_id], avs_result: { code: response[:avs_result] }, - cvv_result: response[:cvv_result]) + cvv_result: response[:cvv_result] + ) end def post_data(action, parameters = {}) diff --git a/lib/active_merchant/billing/gateways/ipg.rb b/lib/active_merchant/billing/gateways/ipg.rb index d16fb86fe62..16637a496ad 100644 --- a/lib/active_merchant/billing/gateways/ipg.rb +++ b/lib/active_merchant/billing/gateways/ipg.rb @@ -2,7 +2,7 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: class IpgGateway < Gateway self.test_url = 'https://test.ipg-online.com/ipgapi/services' - self.live_url = 'https://www5.ipg-online.com' + self.live_url = 'https://www5.ipg-online.com/ipgapi/services' self.supported_countries = %w(AR) self.default_currency = 'ARS' @@ -18,8 +18,8 @@ class IpgGateway < Gateway ACTION_REQUEST_ITEMS = %w(vault unstore) def initialize(options = {}) - requires!(options, :store_id, :user_id, :password, :pem, :pem_password) - @credentials = options + requires!(options, :user_id, :password, :pem, :pem_password) + @credentials = options.merge(store_and_user_id_from(options[:user_id])) @hosted_data_id = nil super end @@ -27,32 +27,32 @@ def initialize(options = {}) def purchase(money, payment, options = {}) xml = build_purchase_and_authorize_request(money, payment, options) - commit('sale', xml) + commit('sale', xml, options) end def authorize(money, payment, options = {}) xml = build_purchase_and_authorize_request(money, payment, options) - commit('preAuth', xml) + commit('preAuth', xml, options) end def capture(money, authorization, options = {}) xml = build_capture_and_refund_request(money, authorization, options) - commit('postAuth', xml) + commit('postAuth', xml, options) end def refund(money, authorization, options = {}) xml = build_capture_and_refund_request(money, authorization, options) - commit('return', xml) + commit('return', xml, options) end def void(authorization, options = {}) xml = Builder::XmlMarkup.new(indent: 2) add_transaction_details(xml, options.merge!({ order_id: authorization })) - commit('void', xml) + commit('void', xml, options) end def store(credit_card, options = {}) @@ -60,7 +60,14 @@ def store(credit_card, options = {}) xml = Builder::XmlMarkup.new(indent: 2) add_storage_item(xml, credit_card, options) - commit('vault', xml) + commit('vault', xml, options) + end + + def unstore(hosted_data_id) + xml = Builder::XmlMarkup.new(indent: 2) + add_unstore_item(xml, hosted_data_id) + + commit('unstore', xml) end def unstore(hosted_data_id) @@ -343,7 +350,12 @@ def ipg_action_namespaces } end - def commit(action, request) + def override_store_id(options) + @credentials[:store_id] = options[:store_id] if options[:store_id].present? + end + + def commit(action, request, options = {}) + override_store_id(options) url = (test? ? test_url : live_url) soap_request = build_soap_request(action, request) response = parse(ssl_post(url, soap_request, build_header)) @@ -390,8 +402,13 @@ def parse_element(reply, node) return reply end + def store_and_user_id_from(user_id) + split_credentials = user_id.split('._.') + { store_id: split_credentials[0].sub(/^WS/, ''), user_id: split_credentials[1] } + end + def message_from(response) - response[:TransactionResult] + [response[:TransactionResult], response[:ErrorMessage]&.split(':')&.last&.strip].compact.join(', ') end def authorization_from(action, response) diff --git a/lib/active_merchant/billing/gateways/iridium.rb b/lib/active_merchant/billing/gateways/iridium.rb index d2f3beff909..d139643f992 100644 --- a/lib/active_merchant/billing/gateways/iridium.rb +++ b/lib/active_merchant/billing/gateways/iridium.rb @@ -376,22 +376,32 @@ def add_merchant_data(xml, options) def commit(request, options) requires!(options, :action) - response = parse(ssl_post(test? ? self.test_url : self.live_url, request, - { 'SOAPAction' => 'https://www.thepaymentgateway.net/' + options[:action], - 'Content-Type' => 'text/xml; charset=utf-8' })) + response = parse( + ssl_post( + test? ? self.test_url : self.live_url, request, + { + 'SOAPAction' => 'https://www.thepaymentgateway.net/' + options[:action], + 'Content-Type' => 'text/xml; charset=utf-8' + } + ) + ) success = response[:transaction_result][:status_code] == '0' message = response[:transaction_result][:message] authorization = success ? [options[:order_id], response[:transaction_output_data][:cross_reference], response[:transaction_output_data][:auth_code]].compact.join(';') : nil - Response.new(success, message, response, + Response.new( + success, + message, + response, test: test?, authorization: authorization, avs_result: { street_match: AVS_CODE[ response[:transaction_output_data][:address_numeric_check_result] ], postal_match: AVS_CODE[ response[:transaction_output_data][:post_code_check_result] ] }, - cvv_result: CVV_CODE[ response[:transaction_output_data][:cv2_check_result] ]) + cvv_result: CVV_CODE[ response[:transaction_output_data][:cv2_check_result] ] + ) end def parse(xml) diff --git a/lib/active_merchant/billing/gateways/itransact.rb b/lib/active_merchant/billing/gateways/itransact.rb index 2a881e1939b..7ad416dc906 100644 --- a/lib/active_merchant/billing/gateways/itransact.rb +++ b/lib/active_merchant/billing/gateways/itransact.rb @@ -387,11 +387,15 @@ def commit(payload) # the Base64 encoded payload signature! response = parse(ssl_post(self.live_url, post_data(payload), 'Content-Type' => 'text/xml')) - Response.new(successful?(response), response[:error_message], response, + Response.new( + successful?(response), + response[:error_message], + response, test: test?, authorization: response[:xid], avs_result: { code: response[:avs_response] }, - cvv_result: response[:cvv_response]) + cvv_result: response[:cvv_response] + ) end def post_data(payload) diff --git a/lib/active_merchant/billing/gateways/iveri.rb b/lib/active_merchant/billing/gateways/iveri.rb index 26e03b45461..87993b01baf 100644 --- a/lib/active_merchant/billing/gateways/iveri.rb +++ b/lib/active_merchant/billing/gateways/iveri.rb @@ -3,7 +3,10 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: class IveriGateway < Gateway + class_attribute :iveri_url + self.live_url = self.test_url = 'https://portal.nedsecure.co.za/iVeriWebService/Service.asmx' + self.iveri_url = 'https://portal.host.iveri.com/iVeriWebService/Service.asmx' self.supported_countries = %w[US ZA GB] self.default_currency = 'ZAR' @@ -91,7 +94,7 @@ def build_xml_envelope(vxml) xml[:soap].Envelope 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/' do xml[:soap].Body do xml.Execute 'xmlns' => 'http://iveri.com/' do - xml.validateRequest 'true' + xml.validateRequest('false') xml.protocol 'V_XML' xml.protocolVersion '2.0' xml.request vxml @@ -118,8 +121,9 @@ def build_vxml_request(action, options) def add_auth_purchase_params(post, money, payment_method, options) add_card_holder_authentication(post, options) add_amount(post, money, options) - add_electronic_commerce_indicator(post, options) + add_electronic_commerce_indicator(post, options) unless options[:three_d_secure] add_payment_method(post, payment_method, options) + add_three_ds(post, options) end def add_amount(post, money, options) @@ -156,7 +160,7 @@ def add_card_holder_authentication(post, options) def commit(post) raw_response = begin - ssl_post(live_url, build_xml_envelope(post), headers(post)) + ssl_post(url, build_xml_envelope(post), headers(post)) rescue ActiveMerchant::ResponseError => e e.response.body end @@ -178,6 +182,10 @@ def mode test? ? 'Test' : 'Live' end + def url + @options[:url_override].to_s == 'iveri' ? iveri_url : live_url + end + def headers(post) { 'Content-Type' => 'text/xml; charset=utf-8', @@ -249,6 +257,34 @@ def underscore(camel_cased_word) tr('-', '_'). downcase end + + def add_three_ds(post, options) + return unless three_d_secure = options[:three_d_secure] + + post.ElectronicCommerceIndicator(formatted_three_ds_eci(three_d_secure[:eci])) if three_d_secure[:eci] + post.CardHolderAuthenticationID(three_d_secure[:xid]) if three_d_secure[:xid] + post.CardHolderAuthenticationData(three_d_secure[:cavv]) if three_d_secure[:cavv] + post.ThreeDSecure_ProtocolVersion(three_d_secure[:version]) if three_d_secure[:version] + post.ThreeDSecure_DSTransID(three_d_secure[:ds_transaction_id]) if three_d_secure[:ds_transaction_id] + post.ThreeDSecure_VEResEnrolled(formatted_enrollment(three_d_secure[:enrolled])) if three_d_secure[:enrolled] + end + + def formatted_enrollment(val) + case val + when 'Y', 'N', 'U' then val + when true, 'true' then 'Y' + when false, 'false' then 'N' + end + end + + def formatted_three_ds_eci(val) + case val + when '05', '02' then 'ThreeDSecure' + when '06', '01' then 'ThreeDSecureAttempted' + when '07' then 'SecureChannel' + else val + end + end end end end diff --git a/lib/active_merchant/billing/gateways/ixopay.rb b/lib/active_merchant/billing/gateways/ixopay.rb index db928d445c9..71e299e726e 100644 --- a/lib/active_merchant/billing/gateways/ixopay.rb +++ b/lib/active_merchant/billing/gateways/ixopay.rb @@ -286,8 +286,8 @@ def commit(request) response = begin parse(ssl_post(url, request, headers(request))) - rescue StandardError => error - parse(error.response.body) + rescue StandardError => e + parse(e.response.body) end Response.new( diff --git a/lib/active_merchant/billing/gateways/jetpay.rb b/lib/active_merchant/billing/gateways/jetpay.rb index 94b6d0bb224..c2b28b5968e 100644 --- a/lib/active_merchant/billing/gateways/jetpay.rb +++ b/lib/active_merchant/billing/gateways/jetpay.rb @@ -284,13 +284,15 @@ def commit(money, request, token = nil) response = parse(ssl_post(url, request)) success = success?(response) - Response.new(success, + Response.new( + success, success ? 'APPROVED' : message_from(response), response, test: test?, authorization: authorization_from(response, money, token), avs_result: { code: response[:avs] }, - cvv_result: response[:cvv2]) + cvv_result: response[:cvv2] + ) end def url diff --git a/lib/active_merchant/billing/gateways/jetpay_v2.rb b/lib/active_merchant/billing/gateways/jetpay_v2.rb index 19ff95a7e99..b852215f181 100644 --- a/lib/active_merchant/billing/gateways/jetpay_v2.rb +++ b/lib/active_merchant/billing/gateways/jetpay_v2.rb @@ -295,14 +295,16 @@ def commit(money, request, token = nil) response = parse(ssl_post(url, request)) success = success?(response) - Response.new(success, + Response.new( + success, success ? 'APPROVED' : message_from(response), response, test: test?, authorization: authorization_from(response, money, token), avs_result: AVSResult.new(code: response[:avs]), cvv_result: CVVResult.new(response[:cvv2]), - error_code: success ? nil : error_code_from(response)) + error_code: success ? nil : error_code_from(response) + ) end def url diff --git a/lib/active_merchant/billing/gateways/kushki.rb b/lib/active_merchant/billing/gateways/kushki.rb index e7306f08802..fbdae802e96 100644 --- a/lib/active_merchant/billing/gateways/kushki.rb +++ b/lib/active_merchant/billing/gateways/kushki.rb @@ -7,7 +7,7 @@ class KushkiGateway < Gateway self.test_url = 'https://api-uat.kushkipagos.com/' self.live_url = 'https://api.kushkipagos.com/' - self.supported_countries = %w[CL CO EC MX PE] + self.supported_countries = %w[BR CL CO EC MX PE] self.default_currency = 'USD' self.money_format = :dollars self.supported_cardtypes = %i[visa master american_express discover diners_club alia] @@ -20,14 +20,14 @@ def initialize(options = {}) def purchase(amount, payment_method, options = {}) MultiResponse.run() do |r| r.process { tokenize(amount, payment_method, options) } - r.process { charge(amount, r.authorization, options) } + r.process { charge(amount, r.authorization, options, payment_method) } end end def authorize(amount, payment_method, options = {}) MultiResponse.run() do |r| r.process { tokenize(amount, payment_method, options) } - r.process { preauthorize(amount, r.authorization, options) } + r.process { preauthorize(amount, r.authorization, options, payment_method) } end end @@ -83,11 +83,13 @@ def tokenize(amount, payment_method, options) add_payment_method(post, payment_method, options) add_full_response(post, options) add_metadata(post, options) + add_months(post, options) + add_deferred(post, options) commit(action, post) end - def charge(amount, authorization, options) + def charge(amount, authorization, options, payment_method = {}) action = 'charge' post = {} @@ -96,11 +98,14 @@ def charge(amount, authorization, options) add_contact_details(post, options[:contact_details]) if options[:contact_details] add_full_response(post, options) add_metadata(post, options) + add_months(post, options) + add_deferred(post, options) + add_three_d_secure(post, payment_method, options) commit(action, post) end - def preauthorize(amount, authorization, options) + def preauthorize(amount, authorization, options, payment_method = {}) action = 'preAuthorization' post = {} @@ -108,6 +113,9 @@ def preauthorize(amount, authorization, options) add_invoice(action, post, amount, options) add_full_response(post, options) add_metadata(post, options) + add_months(post, options) + add_deferred(post, options) + add_three_d_secure(post, payment_method, options) commit(action, post) end @@ -127,9 +135,9 @@ def add_invoice(action, post, money, options) end def add_amount_defaults(sum, money, options) - sum[:subtotalIva] = amount(money).to_f + sum[:subtotalIva] = 0 sum[:iva] = 0 - sum[:subtotalIva0] = 0 + sum[:subtotalIva0] = amount(money).to_f sum[:ice] = 0 if sum[:currency] != 'COP' end @@ -140,7 +148,7 @@ def add_amount_by_country(sum, options) sum[:iva] = amount[:iva].to_f if amount[:iva] sum[:subtotalIva0] = amount[:subtotal_iva_0].to_f if amount[:subtotal_iva_0] sum[:ice] = amount[:ice].to_f if amount[:ice] - if (extra_taxes = amount[:extra_taxes]) && sum[:currency] == 'COP' + if (extra_taxes = amount[:extra_taxes]) sum[:extraTaxes] ||= Hash.new sum[:extraTaxes][:propina] = extra_taxes[:propina].to_f if extra_taxes[:propina] sum[:extraTaxes][:tasaAeroportuaria] = extra_taxes[:tasa_aeroportuaria].to_f if extra_taxes[:tasa_aeroportuaria] @@ -184,6 +192,50 @@ def add_metadata(post, options) post[:metadata] = options[:metadata] if options[:metadata] end + def add_months(post, options) + post[:months] = options[:months] if options[:months] + end + + def add_deferred(post, options) + return unless options[:deferred_grace_months] && options[:deferred_credit_type] && options[:deferred_months] + + post[:deferred] = { + graceMonths: options[:deferred_grace_months], + creditType: options[:deferred_credit_type], + months: options[:deferred_months] + } + end + + def add_three_d_secure(post, payment_method, options) + three_d_secure = options[:three_d_secure] + return unless three_d_secure.present? + + post[:threeDomainSecure] = { + eci: three_d_secure[:eci], + specificationVersion: three_d_secure[:version] + } + + if payment_method.brand == 'master' + post[:threeDomainSecure][:acceptRisk] = three_d_secure[:eci] == '00' + post[:threeDomainSecure][:ucaf] = three_d_secure[:cavv] + post[:threeDomainSecure][:directoryServerTransactionID] = three_d_secure[:ds_transaction_id] + case three_d_secure[:eci] + when '07' + post[:threeDomainSecure][:collectionIndicator] = '0' + when '06' + post[:threeDomainSecure][:collectionIndicator] = '1' + else + post[:threeDomainSecure][:collectionIndicator] = '2' + end + elsif payment_method.brand == 'visa' + post[:threeDomainSecure][:acceptRisk] = three_d_secure[:eci] == '07' + post[:threeDomainSecure][:cavv] = three_d_secure[:cavv] + post[:threeDomainSecure][:xid] = three_d_secure[:xid] if three_d_secure[:xid].present? + else + raise ArgumentError.new 'Kushki supports 3ds2 authentication for only Visa and Mastercard brands.' + end + end + ENDPOINT = { 'tokenize' => 'tokens', 'charge' => 'charges', diff --git a/lib/active_merchant/billing/gateways/linkpoint.rb b/lib/active_merchant/billing/gateways/linkpoint.rb index dc5bf64e56f..11c1b95dc3d 100644 --- a/lib/active_merchant/billing/gateways/linkpoint.rb +++ b/lib/active_merchant/billing/gateways/linkpoint.rb @@ -263,11 +263,15 @@ def scrub(transcript) def commit(money, creditcard, options = {}) response = parse(ssl_post(test? ? self.test_url : self.live_url, post_data(money, creditcard, options))) - Response.new(successful?(response), response[:message], response, + Response.new( + successful?(response), + response[:message], + response, test: test?, authorization: response[:ordernum], avs_result: { code: response[:avs].to_s[2, 1] }, - cvv_result: response[:avs].to_s[3, 1]) + cvv_result: response[:avs].to_s[3, 1] + ) end def successful?(response) diff --git a/lib/active_merchant/billing/gateways/litle.rb b/lib/active_merchant/billing/gateways/litle.rb index 5b97fee20ae..ac5e4c66f5b 100644 --- a/lib/active_merchant/billing/gateways/litle.rb +++ b/lib/active_merchant/billing/gateways/litle.rb @@ -5,9 +5,10 @@ module Billing #:nodoc: class LitleGateway < Gateway SCHEMA_VERSION = '9.14' - class_attribute :postlive_url + class_attribute :postlive_url, :prelive_url self.test_url = 'https://www.testvantivcnp.com/sandbox/communicator/online' + self.prelive_url = 'https://payments.vantivprelive.com/vap/communicator/online' self.postlive_url = 'https://payments.vantivpostlive.com/vap/communicator/online' self.live_url = 'https://payments.vantivcnp.com/vap/communicator/online' @@ -15,7 +16,7 @@ class LitleGateway < Gateway self.default_currency = 'USD' self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] - self.homepage_url = 'http://www.vantiv.com/' + self.homepage_url = 'https://www.fisglobal.com/' self.display_name = 'Vantiv eCommerce' def initialize(options = {}) @@ -320,6 +321,7 @@ def add_auth_purchase_params(doc, money, payment_method, options) add_merchant_data(doc, options) add_debt_repayment(doc, options) add_stored_credential_params(doc, options) + add_fraud_filter_override(doc, options) end def add_credit_params(doc, money, payment_method, options) @@ -365,6 +367,10 @@ def add_debt_repayment(doc, options) doc.debtRepayment(true) if options[:debt_repayment] == true end + def add_fraud_filter_override(doc, options) + doc.fraudFilterOverride(options[:fraud_filter_override]) if options[:fraud_filter_override] + end + def add_payment_method(doc, payment_method, options) if payment_method.is_a?(String) doc.token do @@ -532,8 +538,10 @@ def format_exp_date(month, year) def parse(kind, xml) parsed = {} - doc = Nokogiri::XML(xml).remove_namespaces! + + parsed['duplicate'] = doc.at_xpath('//saleResponse').try(:[], 'duplicate') == 'true' if kind == :sale + doc.xpath("//litleOnlineResponse/#{kind}Response/*").each do |node| if node.elements.empty? parsed[node.name.to_sym] = node.text @@ -564,15 +572,26 @@ def commit(kind, request, money = nil) cvv_result: parsed[:fraudResult_cardValidationResult] } - Response.new(success_from(kind, parsed), parsed[:message], parsed, options) + Response.new(success_from(kind, parsed), message_from(parsed), parsed, options) end def success_from(kind, parsed) - return (parsed[:response] == '000') unless kind == :registerToken + return %w(000 001 010).any?(parsed[:response]) unless kind == :registerToken %w(000 801 802).include?(parsed[:response]) end + def message_from(parsed) + case parsed[:response] + when '010' + return "#{parsed[:message]}: The authorized amount is less than the requested amount." + when '001' + return "#{parsed[:message]}: This is sent to acknowledge that the submitted transaction has been received." + else + parsed[:message] + end + end + def authorization_from(kind, parsed, money) kind == :registerToken ? parsed[:litleToken] : "#{parsed[:litleTxnId]};#{kind};#{money}" end @@ -609,6 +628,7 @@ def build_xml_request def url return postlive_url if @options[:url_override].to_s == 'postlive' + return prelive_url if @options[:url_override].to_s == 'prelive' test? ? test_url : live_url end diff --git a/lib/active_merchant/billing/gateways/mastercard.rb b/lib/active_merchant/billing/gateways/mastercard.rb index be18bda516f..894ad2be3f5 100644 --- a/lib/active_merchant/billing/gateways/mastercard.rb +++ b/lib/active_merchant/billing/gateways/mastercard.rb @@ -10,7 +10,7 @@ def purchase(amount, payment_method, options = {}) if options[:pay_mode] post = new_post add_invoice(post, amount, options) - add_reference(post, *new_authorization) + add_reference(post, *new_authorization(options)) add_payment_method(post, payment_method) add_customer_data(post, payment_method, options) add_3dsecure_id(post, options) @@ -27,7 +27,7 @@ def purchase(amount, payment_method, options = {}) def authorize(amount, payment_method, options = {}) post = new_post add_invoice(post, amount, options) - add_reference(post, *new_authorization) + add_reference(post, *new_authorization(options)) add_payment_method(post, payment_method) add_customer_data(post, payment_method, options) add_3dsecure_id(post, options) @@ -218,14 +218,7 @@ def build_url(orderid, transactionid) def base_url if test? - case @options[:region] - when 'asia_pacific' - test_ap_url - when 'europe' - test_eu_url - when 'north_america', nil - test_na_url - end + test_url else case @options[:region] when 'asia_pacific' @@ -271,9 +264,9 @@ def split_authorization(authorization) authorization.split('|') end - def new_authorization + def new_authorization(options) # Must be unique within a merchant id. - orderid = SecureRandom.uuid + orderid = options[:order_id] || SecureRandom.uuid # Must be unique within an order id. transactionid = '1' diff --git a/lib/active_merchant/billing/gateways/mercado_pago.rb b/lib/active_merchant/billing/gateways/mercado_pago.rb index a3723197ae0..36949c0422e 100644 --- a/lib/active_merchant/billing/gateways/mercado_pago.rb +++ b/lib/active_merchant/billing/gateways/mercado_pago.rb @@ -61,6 +61,10 @@ def verify(credit_card, options = {}) end end + def inquire(authorization, options = {}) + commit('inquire', inquire_path(authorization, options), {}) + end + def supports_scrubbing? true end @@ -261,6 +265,10 @@ def parse(body) def commit(action, path, parameters) if %w[capture void].include?(action) response = parse(ssl_request(:put, url(path), post_data(parameters), headers)) + elsif action == 'inquire' + response = parse(ssl_get(url(path), headers)) + + response = response[0]['results'][0] if response.is_a?(Array) else response = parse(ssl_post(url(path), post_data(parameters), headers(parameters))) end @@ -295,6 +303,15 @@ def post_data(parameters = {}) parameters.clone.tap { |p| p.delete(:device_id) }.to_json end + def inquire_path(authorization, options) + if authorization + authorization, = authorization.split('|') + "payments/#{authorization}" + else + "payments/search?external_reference=#{options[:order_id] || options[:external_reference]}" + end + end + def error_code_from(action, response) unless success_from(action, response) if cause = response['cause'] diff --git a/lib/active_merchant/billing/gateways/merchant_e_solutions.rb b/lib/active_merchant/billing/gateways/merchant_e_solutions.rb index d268e590187..a5bf6bdce81 100644 --- a/lib/active_merchant/billing/gateways/merchant_e_solutions.rb +++ b/lib/active_merchant/billing/gateways/merchant_e_solutions.rb @@ -18,6 +18,8 @@ class MerchantESolutionsGateway < Gateway # The name of the gateway self.display_name = 'Merchant e-Solutions' + SUCCESS_RESPONSE_CODES = %w(000 085) + def initialize(options = {}) requires!(options, :login, :password) super @@ -25,23 +27,21 @@ def initialize(options = {}) def authorize(money, creditcard_or_card_id, options = {}) post = {} - post[:client_reference_number] = options[:customer] if options.has_key?(:customer) - post[:moto_ecommerce_ind] = options[:moto_ecommerce_ind] if options.has_key?(:moto_ecommerce_ind) add_invoice(post, options) add_payment_source(post, creditcard_or_card_id, options) add_address(post, options) add_3dsecure_params(post, options) + add_stored_credentials(post, options) commit('P', money, post) end def purchase(money, creditcard_or_card_id, options = {}) post = {} - post[:client_reference_number] = options[:customer] if options.has_key?(:customer) - post[:moto_ecommerce_ind] = options[:moto_ecommerce_ind] if options.has_key?(:moto_ecommerce_ind) add_invoice(post, options) add_payment_source(post, creditcard_or_card_id, options) add_address(post, options) add_3dsecure_params(post, options) + add_stored_credentials(post, options) commit('D', money, post) end @@ -55,10 +55,10 @@ def capture(money, transaction_id, options = {}) end def store(creditcard, options = {}) - post = {} - post[:client_reference_number] = options[:customer] if options.has_key?(:customer) - add_creditcard(post, creditcard, options) - commit('T', nil, post) + MultiResponse.run do |r| + r.process { temporary_store(creditcard, options) } + r.process { verify(r.authorization, { store_card: 'y' }) } + end end def unstore(card_id, options = {}) @@ -94,6 +94,13 @@ def void(transaction_id, options = {}) commit('V', nil, options.merge(post)) end + def verify(credit_card, options = {}) + post = {} + post[:store_card] = options[:store_card] if options[:store_card] + add_payment_source(post, credit_card, options) + commit('A', 0, post) + end + def supports_scrubbing? true end @@ -107,6 +114,13 @@ def scrub(transcript) private + def temporary_store(creditcard, options = {}) + post = {} + post[:client_reference_number] = options[:customer] if options.has_key?(:customer) + add_creditcard(post, creditcard, options) + commit('T', nil, post) + end + def add_address(post, options) if address = options[:billing_address] || options[:address] post[:cardholder_street_address] = address[:address1].to_s.gsub(/[^\w.]/, '+') @@ -145,6 +159,16 @@ def add_3dsecure_params(post, options) post[:ucaf_auth_data] = options[:ucaf_auth_data] unless empty?(options[:ucaf_auth_data]) end + def add_stored_credentials(post, options) + post[:client_reference_number] = options[:client_reference_number] if options[:client_reference_number] + post[:moto_ecommerce_ind] = options[:moto_ecommerce_ind] if options[:moto_ecommerce_ind] + post[:recurring_pmt_num] = options[:recurring_pmt_num] if options[:recurring_pmt_num] + post[:recurring_pmt_count] = options[:recurring_pmt_count] if options[:recurring_pmt_count] + post[:card_on_file] = options[:card_on_file] if options[:card_on_file] + post[:cit_mit_indicator] = options[:cit_mit_indicator] if options[:cit_mit_indicator] + post[:account_data_source] = options[:account_data_source] if options[:account_data_source] + end + def parse(body) results = {} body.split(/&/).each do |pair| @@ -165,11 +189,25 @@ def commit(action, money, parameters) { 'error_code' => '404', 'auth_response_text' => e.to_s } end - Response.new(response['error_code'] == '000', message_from(response), response, - authorization: response['transaction_id'], + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), test: test?, cvv_result: response['cvv2_result'], - avs_result: { code: response['avs_result'] }) + avs_result: { code: response['avs_result'] } + ) + end + + def authorization_from(response) + return response['card_id'] if response['card_id'] + + response['transaction_id'] + end + + def success_from(response) + SUCCESS_RESPONSE_CODES.include?(response['error_code']) end def message_from(response) diff --git a/lib/active_merchant/billing/gateways/merchant_ware.rb b/lib/active_merchant/billing/gateways/merchant_ware.rb index 1781a301968..cc934aa6fd7 100644 --- a/lib/active_merchant/billing/gateways/merchant_ware.rb +++ b/lib/active_merchant/billing/gateways/merchant_ware.rb @@ -290,19 +290,26 @@ def url(v4 = false) def commit(action, request, v4 = false) begin - data = ssl_post(url(v4), request, + data = ssl_post( + url(v4), + request, 'Content-Type' => 'text/xml; charset=utf-8', - 'SOAPAction' => soap_action(action, v4)) + 'SOAPAction' => soap_action(action, v4) + ) response = parse(action, data) rescue ActiveMerchant::ResponseError => e response = parse_error(e.response) end - Response.new(response[:success], response[:message], response, + Response.new( + response[:success], + response[:message], + response, test: test?, authorization: authorization_from(response), avs_result: { code: response['AVSResponse'] }, - cvv_result: response['CVResponse']) + cvv_result: response['CVResponse'] + ) end def authorization_from(response) diff --git a/lib/active_merchant/billing/gateways/merchant_ware_version_four.rb b/lib/active_merchant/billing/gateways/merchant_ware_version_four.rb index 36635dd0f2a..9657a5631ed 100644 --- a/lib/active_merchant/billing/gateways/merchant_ware_version_four.rb +++ b/lib/active_merchant/billing/gateways/merchant_ware_version_four.rb @@ -261,19 +261,26 @@ def url def commit(action, request) begin - data = ssl_post(url, request, + data = ssl_post( + url, + request, 'Content-Type' => 'text/xml; charset=utf-8', - 'SOAPAction' => soap_action(action)) + 'SOAPAction' => soap_action(action) + ) response = parse(action, data) rescue ActiveMerchant::ResponseError => e response = parse_error(e.response, action) end - Response.new(response[:success], response[:message], response, + Response.new( + response[:success], + response[:message], + response, test: test?, authorization: authorization_from(response), avs_result: { code: response['AvsResponse'] }, - cvv_result: response['CvResponse']) + cvv_result: response['CvResponse'] + ) end def authorization_from(response) diff --git a/lib/active_merchant/billing/gateways/mercury.rb b/lib/active_merchant/billing/gateways/mercury.rb index fcbca035e0c..5648640d734 100644 --- a/lib/active_merchant/billing/gateways/mercury.rb +++ b/lib/active_merchant/billing/gateways/mercury.rb @@ -302,12 +302,16 @@ def commit(action, request) success = SUCCESS_CODES.include?(response[:cmd_status]) message = success ? 'Success' : message_from(response) - Response.new(success, message, response, + Response.new( + success, + message, + response, test: test?, authorization: authorization_from(response), avs_result: { code: response[:avs_result] }, cvv_result: response[:cvv_result], - error_code: success ? nil : STANDARD_ERROR_CODE_MAPPING[response[:dsix_return_code]]) + error_code: success ? nil : STANDARD_ERROR_CODE_MAPPING[response[:dsix_return_code]] + ) end def message_from(response) diff --git a/lib/active_merchant/billing/gateways/metrics_global.rb b/lib/active_merchant/billing/gateways/metrics_global.rb index 5aac5401d4b..c5b28a94990 100644 --- a/lib/active_merchant/billing/gateways/metrics_global.rb +++ b/lib/active_merchant/billing/gateways/metrics_global.rb @@ -175,12 +175,16 @@ def commit(action, money, parameters) # (TESTMODE) Successful Sale test_mode = test? || message =~ /TESTMODE/ - Response.new(success?(response), message, response, + Response.new( + success?(response), + message, + response, test: test_mode, authorization: response[:transaction_id], fraud_review: fraud_review?(response), avs_result: { code: response[:avs_result_code] }, - cvv_result: response[:card_code]) + cvv_result: response[:card_code] + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/migs.rb b/lib/active_merchant/billing/gateways/migs.rb index 50a254e49a3..f50b3d29de5 100644 --- a/lib/active_merchant/billing/gateways/migs.rb +++ b/lib/active_merchant/billing/gateways/migs.rb @@ -281,12 +281,16 @@ def response_object(response) cvv_result_code = response[:CSCResultCode] cvv_result_code = 'P' if cvv_result_code == 'Unsupported' - Response.new(success?(response), response[:Message], response, + Response.new( + success?(response), + response[:Message], + response, test: test?, authorization: response[:TransactionNo], fraud_review: fraud_review?(response), avs_result: { code: avs_response_code }, - cvv_result: cvv_result_code) + cvv_result: cvv_result_code + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/mit.rb b/lib/active_merchant/billing/gateways/mit.rb index 74b7bf6beab..785be9368da 100644 --- a/lib/active_merchant/billing/gateways/mit.rb +++ b/lib/active_merchant/billing/gateways/mit.rb @@ -93,8 +93,7 @@ def authorize(money, payment, options = {}) post_to_json_encrypt = encrypt(post_to_json, @options[:key_session]) final_post = '' + post_to_json_encrypt + '' + @options[:user] + '' - json_post = {} - json_post[:payload] = final_post + json_post = final_post commit('sale', json_post) end @@ -114,8 +113,7 @@ def capture(money, authorization, options = {}) post_to_json_encrypt = encrypt(post_to_json, @options[:key_session]) final_post = '' + post_to_json_encrypt + '' + @options[:user] + '' - json_post = {} - json_post[:payload] = final_post + json_post = final_post commit('capture', json_post) end @@ -136,8 +134,7 @@ def refund(money, authorization, options = {}) post_to_json_encrypt = encrypt(post_to_json, @options[:key_session]) final_post = '' + post_to_json_encrypt + '' + @options[:user] + '' - json_post = {} - json_post[:payload] = final_post + json_post = final_post commit('refund', json_post) end @@ -145,10 +142,18 @@ def supports_scrubbing? true end + def extract_mit_responses_from_transcript(transcript) + groups = transcript.scan(/reading \d+ bytes(.*?)read \d+ bytes/m) + groups.map do |group| + group.first.scan(/-> "(.*?)"/).flatten.map(&:strip).join('') + end + end + def scrub(transcript) ret_transcript = transcript auth_origin = ret_transcript[/(.*?)<\/authorization>/, 1] unless auth_origin.nil? + auth_origin = auth_origin.gsub('\n', '') auth_decrypted = decrypt(auth_origin, @options[:key_session]) auth_json = JSON.parse(auth_decrypted) auth_json['card'] = '[FILTERED]' @@ -162,6 +167,7 @@ def scrub(transcript) cap_origin = ret_transcript[/(.*?)<\/capture>/, 1] unless cap_origin.nil? + cap_origin = cap_origin.gsub('\n', '') cap_decrypted = decrypt(cap_origin, @options[:key_session]) cap_json = JSON.parse(cap_decrypted) cap_json['apikey'] = '[FILTERED]' @@ -173,6 +179,7 @@ def scrub(transcript) ref_origin = ret_transcript[/(.*?)<\/refund>/, 1] unless ref_origin.nil? + ref_origin = ref_origin.gsub('\n', '') ref_decrypted = decrypt(ref_origin, @options[:key_session]) ref_json = JSON.parse(ref_decrypted) ref_json['apikey'] = '[FILTERED]' @@ -182,15 +189,10 @@ def scrub(transcript) ret_transcript = ret_transcript.gsub(/(.*?)<\/refund>/, ref_tagged) end - res_origin = ret_transcript[/#{Regexp.escape('reading ')}(.*?)#{Regexp.escape('read')}/m, 1] - loop do - break if res_origin.nil? - - resp_origin = res_origin[/#{Regexp.escape('"')}(.*?)#{Regexp.escape('"')}/m, 1] - resp_decrypted = decrypt(resp_origin, @options[:key_session]) - ret_transcript[/#{Regexp.escape('reading ')}(.*?)#{Regexp.escape('read')}/m, 1] = resp_decrypted - ret_transcript = ret_transcript.sub('reading ', 'response: ') - res_origin = ret_transcript[/#{Regexp.escape('reading ')}(.*?)#{Regexp.escape('read')}/m, 1] + groups = extract_mit_responses_from_transcript(transcript) + groups.each do |group| + group_decrypted = decrypt(group, @options[:key_session]) + ret_transcript = ret_transcript.gsub('Conn close', "\n" + group_decrypted + "\nConn close") end ret_transcript @@ -219,9 +221,7 @@ def add_payment(post, payment) end def commit(action, parameters) - json_str = JSON.generate(parameters) - cleaned_str = json_str.gsub('\n', '') - raw_response = ssl_post(live_url, cleaned_str, { 'Content-type' => 'application/json' }) + raw_response = ssl_post(live_url, parameters, { 'Content-type' => 'text/plain' }) response = JSON.parse(decrypt(raw_response, @options[:key_session])) Response.new( diff --git a/lib/active_merchant/billing/gateways/modern_payments_cim.rb b/lib/active_merchant/billing/gateways/modern_payments_cim.rb index d5d6f9b2a27..9a8f760ec8e 100644 --- a/lib/active_merchant/billing/gateways/modern_payments_cim.rb +++ b/lib/active_merchant/billing/gateways/modern_payments_cim.rb @@ -111,13 +111,12 @@ def add_credit_card(post, credit_card) end def build_request(action, params) + envelope_obj = { 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', + 'xmlns:env' => 'http://schemas.xmlsoap.org/soap/envelope/', + 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance' } xml = Builder::XmlMarkup.new indent: 2 xml.instruct! - xml.tag! 'env:Envelope', - { 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:env' => 'http://schemas.xmlsoap.org/soap/envelope/', - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance' } do - + xml.tag! 'env:Envelope', envelope_obj do xml.tag! 'env:Body' do xml.tag! action, { 'xmlns' => xmlns(action) } do xml.tag! 'clientId', @options[:login] @@ -146,15 +145,24 @@ def url(action) end def commit(action, params) - data = ssl_post(url(action), build_request(action, params), - { 'Content-Type' => 'text/xml; charset=utf-8', - 'SOAPAction' => "#{xmlns(action)}#{action}" }) + data = ssl_post( + url(action), + build_request(action, params), + { + 'Content-Type' => 'text/xml; charset=utf-8', + 'SOAPAction' => "#{xmlns(action)}#{action}" + } + ) response = parse(action, data) - Response.new(successful?(action, response), message_from(action, response), response, + Response.new( + successful?(action, response), + message_from(action, response), + response, test: test?, authorization: authorization_from(action, response), - avs_result: { code: response[:avs_code] }) + avs_result: { code: response[:avs_code] } + ) end def authorization_from(action, response) diff --git a/lib/active_merchant/billing/gateways/monei.rb b/lib/active_merchant/billing/gateways/monei.rb index 4d496e0d3b5..c7e1a5b9b5a 100755 --- a/lib/active_merchant/billing/gateways/monei.rb +++ b/lib/active_merchant/billing/gateways/monei.rb @@ -294,6 +294,7 @@ def add_3ds2_authenticated_data(request, options) def add_browser_info(request, options) request[:sessionDetails][:ip] = options[:ip] if options[:ip] request[:sessionDetails][:userAgent] = options[:user_agent] if options[:user_agent] + request[:sessionDetails][:lang] = options[:lang] if options[:lang] end # Private: Parse JSON response from Monei servers diff --git a/lib/active_merchant/billing/gateways/moneris.rb b/lib/active_merchant/billing/gateways/moneris.rb index 2139b955f74..53629e02195 100644 --- a/lib/active_merchant/billing/gateways/moneris.rb +++ b/lib/active_merchant/billing/gateways/moneris.rb @@ -9,6 +9,8 @@ module Billing #:nodoc: # Response Values", available at Moneris' {eSelect Plus Documentation # Centre}[https://www3.moneris.com/connect/en/documents/index.html]. class MonerisGateway < Gateway + WALLETS = %w(APP GPP) + self.test_url = 'https://esqa.moneris.com/gateway2/servlet/MpgRequest' self.live_url = 'https://www3.moneris.com/gateway2/servlet/MpgRequest' @@ -47,7 +49,7 @@ def authorize(money, creditcard_or_datakey, options = {}) post = {} add_payment_source(post, creditcard_or_datakey, options) post[:amount] = amount(money) - post[:order_id] = options[:order_id] + post[:order_id] = format_order_id(post[:wallet_indicator], options[:order_id]) post[:address] = options[:billing_address] || options[:address] post[:crypt_type] = options[:crypt_type] || @options[:crypt_type] add_external_mpi_fields(post, options) @@ -71,7 +73,7 @@ def purchase(money, creditcard_or_datakey, options = {}) post = {} add_payment_source(post, creditcard_or_datakey, options) post[:amount] = amount(money) - post[:order_id] = options[:order_id] + post[:order_id] = format_order_id(post[:wallet_indicator], options[:order_id]) post[:address] = options[:billing_address] || options[:address] post[:crypt_type] = options[:crypt_type] || @options[:crypt_type] add_external_mpi_fields(post, options) @@ -431,10 +433,23 @@ def credential_on_file(parameters) end def wallet_indicator(token_source) - return 'APP' if token_source == 'apple_pay' - return 'ANP' if token_source == 'android_pay' + return { + 'apple_pay' => 'APP', + 'google_pay' => 'GPP', + 'android_pay' => 'ANP' + }[token_source] + end + + def format_order_id(wallet_indicator_code, order_id = nil) + # Truncate (max 100 characters) order id for + # google pay and apple pay (specific wallets / token sources) + return truncate_order_id(order_id) if WALLETS.include?(wallet_indicator_code) + + order_id + end - nil + def truncate_order_id(order_id = nil) + order_id.present? ? order_id[0, 100] : SecureRandom.alphanumeric(100) end def message_from(message) @@ -452,8 +467,8 @@ def actions 'indrefund' => %i[order_id cust_id amount pan expdate crypt_type], 'completion' => %i[order_id comp_amount txn_number crypt_type], 'purchasecorrection' => %i[order_id txn_number crypt_type], - 'cavv_preauth' => %i[order_id cust_id amount pan expdate cavv crypt_type wallet_indicator], - 'cavv_purchase' => %i[order_id cust_id amount pan expdate cavv crypt_type wallet_indicator], + 'cavv_preauth' => %i[order_id cust_id amount pan expdate cavv crypt_type wallet_indicator threeds_version threeds_server_trans_id ds_trans_id], + 'cavv_purchase' => %i[order_id cust_id amount pan expdate cavv crypt_type wallet_indicator threeds_version threeds_server_trans_id ds_trans_id], 'card_verification' => %i[order_id cust_id pan expdate crypt_type avs_info cvd_info cof_info], 'transact' => %i[order_id cust_id amount pan expdate crypt_type], 'Batchcloseall' => [], diff --git a/lib/active_merchant/billing/gateways/money_movers.rb b/lib/active_merchant/billing/gateways/money_movers.rb index 0a16b4ea5dc..2ba6c35cae7 100644 --- a/lib/active_merchant/billing/gateways/money_movers.rb +++ b/lib/active_merchant/billing/gateways/money_movers.rb @@ -113,11 +113,15 @@ def commit(action, money, parameters) response = parse(data) message = message_from(response) - Response.new(success?(response), message, response, + Response.new( + success?(response), + message, + response, test: test?, authorization: response['transactionid'], avs_result: { code: response['avsresponse'] }, - cvv_result: response['cvvresponse']) + cvv_result: response['cvvresponse'] + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/mundipagg.rb b/lib/active_merchant/billing/gateways/mundipagg.rb index 331421d4a1d..1549a3ea4af 100644 --- a/lib/active_merchant/billing/gateways/mundipagg.rb +++ b/lib/active_merchant/billing/gateways/mundipagg.rb @@ -60,11 +60,13 @@ def capture(money, authorization, options = {}) post = {} post[:code] = authorization add_invoice(post, money, options) + add_auth_key(post, options) commit('capture', post, authorization) end def refund(money, authorization, options = {}) add_invoice(post = {}, money, options) + add_auth_key(post, options) commit('refund', post, authorization) end @@ -77,6 +79,7 @@ def store(payment, options = {}) options.update(name: payment.name) options = add_customer(options) unless options[:customer_id] add_payment(post, payment, options) + add_auth_key(post, options) commit('store', post, options[:customer_id]) end diff --git a/lib/active_merchant/billing/gateways/nab_transact.rb b/lib/active_merchant/billing/gateways/nab_transact.rb index 723063781a0..7c81d230bcf 100644 --- a/lib/active_merchant/billing/gateways/nab_transact.rb +++ b/lib/active_merchant/billing/gateways/nab_transact.rb @@ -233,16 +233,24 @@ def build_unstore_request(identification, options) def commit(action, request) response = parse(ssl_post(test? ? self.test_url : self.live_url, build_request(action, request))) - Response.new(success?(response), message_from(response), response, + Response.new( + success?(response), + message_from(response), + response, test: test?, - authorization: authorization_from(action, response)) + authorization: authorization_from(action, response) + ) end def commit_periodic(action, request) response = parse(ssl_post(test? ? self.test_periodic_url : self.live_periodic_url, build_periodic_request(action, request))) - Response.new(success?(response), message_from(response), response, + Response.new( + success?(response), + message_from(response), + response, test: test?, - authorization: authorization_from(action, response)) + authorization: authorization_from(action, response) + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/net_registry.rb b/lib/active_merchant/billing/gateways/net_registry.rb index bb3c11d63f3..7052581b7ba 100644 --- a/lib/active_merchant/billing/gateways/net_registry.rb +++ b/lib/active_merchant/billing/gateways/net_registry.rb @@ -144,8 +144,12 @@ def commit(action, params) # get gateway response response = parse(ssl_post(self.live_url, post_data(action, params))) - Response.new(response['status'] == 'approved', message_from(response), response, - authorization: authorization_from(response, action)) + Response.new( + response['status'] == 'approved', + message_from(response), + response, + authorization: authorization_from(response, action) + ) end def post_data(action, params) diff --git a/lib/active_merchant/billing/gateways/netbilling.rb b/lib/active_merchant/billing/gateways/netbilling.rb index 08f0e57a398..dfa479a495d 100644 --- a/lib/active_merchant/billing/gateways/netbilling.rb +++ b/lib/active_merchant/billing/gateways/netbilling.rb @@ -193,11 +193,15 @@ def parse(body) def commit(action, parameters) response = parse(ssl_post(self.live_url, post_data(action, parameters))) - Response.new(success?(response), message_from(response), response, + Response.new( + success?(response), + message_from(response), + response, test: test_response?(response), authorization: response[:trans_id], avs_result: { code: response[:avs_code] }, - cvv_result: response[:cvv2_code]) + cvv_result: response[:cvv2_code] + ) rescue ActiveMerchant::ResponseError => e raise unless e.response.code =~ /^[67]\d\d$/ diff --git a/lib/active_merchant/billing/gateways/network_merchants.rb b/lib/active_merchant/billing/gateways/network_merchants.rb index 4c1c2ef16d5..a72f133dce0 100644 --- a/lib/active_merchant/billing/gateways/network_merchants.rb +++ b/lib/active_merchant/billing/gateways/network_merchants.rb @@ -200,11 +200,15 @@ def commit(action, parameters) authorization = authorization_from(success, parameters, raw) - Response.new(success, raw['responsetext'], raw, + Response.new( + success, + raw['responsetext'], + raw, test: test?, authorization: authorization, avs_result: { code: raw['avsresponse'] }, - cvv_result: raw['cvvresponse']) + cvv_result: raw['cvvresponse'] + ) end def build_request(action, parameters) diff --git a/lib/active_merchant/billing/gateways/nmi.rb b/lib/active_merchant/billing/gateways/nmi.rb index 0268ae4bb23..aee8fa754a7 100644 --- a/lib/active_merchant/billing/gateways/nmi.rb +++ b/lib/active_merchant/billing/gateways/nmi.rb @@ -149,6 +149,7 @@ def add_level3_fields(post, options) def add_invoice(post, money, options) post[:amount] = amount(money) + post[:surcharge] = options[:surcharge] if options[:surcharge] post[:orderid] = options[:order_id] post[:orderdescription] = options[:description] post[:currency] = options[:currency] || currency(money) @@ -232,6 +233,9 @@ def add_customer_data(post, options) end if (shipping_address = options[:shipping_address]) + first_name, last_name = split_names(shipping_address[:name]) + post[:shipping_firstname] = first_name if first_name + post[:shipping_lastname] = last_name if last_name post[:shipping_company] = shipping_address[:company] post[:shipping_address1] = shipping_address[:address1] post[:shipping_address2] = shipping_address[:address2] @@ -240,6 +244,7 @@ def add_customer_data(post, options) post[:shipping_country] = shipping_address[:country] post[:shipping_zip] = shipping_address[:zip] post[:shipping_phone] = shipping_address[:phone] + post[:shipping_email] = options[:shipping_email] if options[:shipping_email] end if (descriptor = options[:descriptors]) diff --git a/lib/active_merchant/billing/gateways/ogone.rb b/lib/active_merchant/billing/gateways/ogone.rb index 3223f5f41c8..898c2ca8cd9 100644 --- a/lib/active_merchant/billing/gateways/ogone.rb +++ b/lib/active_merchant/billing/gateways/ogone.rb @@ -136,7 +136,7 @@ class OgoneGateway < Gateway self.supported_countries = %w[BE DE FR NL AT CH] # also supports Airplus and UATP self.supported_cardtypes = %i[visa master american_express diners_club discover jcb maestro] - self.homepage_url = 'http://www.ogone.com/' + self.homepage_url = 'https://www.ingenico.com/login/ogone/' self.display_name = 'Ogone' self.default_currency = 'EUR' self.money_format = :cents @@ -263,8 +263,7 @@ def perform_non_referenced_credit(money, payment_target, options = {}) end def add_payment_source(post, payment_source, options) - add_d3d(post, options) if options[:d3d] - + add_d3d(post, options) if options[:d3d] || three_d_secure(options) if payment_source.is_a?(String) add_alias(post, payment_source, options[:alias_operation]) add_eci(post, options[:eci] || '9') @@ -285,8 +284,6 @@ def add_d3d(post, options) THREE_D_SECURE_DISPLAY_WAYS[options[:win_3ds]] : THREE_D_SECURE_DISPLAY_WAYS[:main_window] add_pair post, 'WIN3DS', win_3ds - - add_pair post, 'HTTP_ACCEPT', options[:http_accept] || '*/*' add_pair post, 'HTTP_USER_AGENT', options[:http_user_agent] if options[:http_user_agent] add_pair post, 'ACCEPTURL', options[:accept_url] if options[:accept_url] add_pair post, 'DECLINEURL', options[:decline_url] if options[:decline_url] @@ -296,6 +293,37 @@ def add_d3d(post, options) add_pair post, 'PARAMPLUS', options[:paramplus] if options[:paramplus] add_pair post, 'COMPLUS', options[:complus] if options[:complus] add_pair post, 'LANGUAGE', options[:language] if options[:language] + if options[:three_ds_2] + browser_info = options[:three_ds_2][:browser_info] + ecom_postal = options[:billing_address] + if browser_info + add_pair post, 'BROWSERACCEPTHEADER', browser_info[:accept_header] + add_pair post, 'BROWSERCOLORDEPTH', browser_info[:depth] + + # for 3ds v2.1 to v2.2 add BROWSERJAVASCRIPTENABLED: This boolean indicates whether your customers have enabled JavaScript in their browsers when making a purchase. + # the following BROWSER parameters will remain mandatory unless browser_info[:javascript] = false + # her documentation https://epayments-support.ingenico.com/en/integration-solutions/integrations/directlink#directlink_integration_guides_secure_payment_with_3_d_secure + add_pair post, 'BROWSERJAVASCRIPTENABLED', browser_info[:javascript] + add_pair post, 'BROWSERJAVAENABLED', browser_info[:java] + add_pair post, 'BROWSERLANGUAGE', browser_info[:language] + add_pair post, 'BROWSERSCREENHEIGHT', browser_info[:height] + add_pair post, 'BROWSERSCREENWIDTH', browser_info[:width] + add_pair post, 'BROWSERTIMEZONE', browser_info[:timezone] + add_pair post, 'BROWSERUSERAGENT', browser_info[:user_agent] + end + # recommended + if ecom_postal + add_pair post, 'ECOM_BILLTO_POSTAL_CITY', ecom_postal[:city] + add_pair post, 'ECOM_BILLTO_POSTAL_COUNTRYCODE', ecom_postal[:country] + add_pair post, 'ECOM_BILLTO_POSTAL_STREET_LINE1', ecom_postal[:address1] + add_pair post, 'ECOM_BILLTO_POSTAL_STREET_LINE2', ecom_postal[:address2] + add_pair post, 'ECOM_BILLTO_POSTAL_POSTALCODE', ecom_postal[:zip] + end + # optional + add_pair post, 'Mpi.threeDSRequestorChallengeIndicator', options[:three_ds_reqchallengeind] + else + add_pair post, 'HTTP_ACCEPT', options[:http_accept] || '*/*' + end end def add_eci(post, eci) @@ -414,7 +442,7 @@ def add_signature(parameters) return end - add_pair parameters, 'SHASign', calculate_signature(parameters, @options[:signature_encryptor], @options[:signature]) + add_pair parameters, 'SHASIGN', calculate_signature(parameters, @options[:signature_encryptor], @options[:signature]) end def calculate_signature(signed_parameters, algorithm, secret) @@ -432,7 +460,7 @@ def calculate_signature(signed_parameters, algorithm, secret) raise "Unknown signature algorithm #{algorithm}" end - filtered_params = signed_parameters.select { |_k, v| !v.blank? } + filtered_params = signed_parameters.compact sha_encryptor.hexdigest( filtered_params.sort_by { |k, _v| k.upcase }.map { |k, v| "#{k.upcase}=#{v}#{secret}" }.join('') ).upcase @@ -456,7 +484,7 @@ def legacy_calculate_signature(parameters, secret) end def add_pair(post, key, value) - post[key] = value if !value.blank? + post[key] = value unless value.nil? end def convert_attributes_to_hash(rexml_attributes) @@ -466,6 +494,10 @@ def convert_attributes_to_hash(rexml_attributes) end response_hash end + + def three_d_secure(options) + options[:three_d_secure] ? options[:three_d_secure][:required] : false + end end class OgoneResponse < Response diff --git a/lib/active_merchant/billing/gateways/openpay.rb b/lib/active_merchant/billing/gateways/openpay.rb index 630e0ca111d..e655a5b599a 100644 --- a/lib/active_merchant/billing/gateways/openpay.rb +++ b/lib/active_merchant/billing/gateways/openpay.rb @@ -1,8 +1,15 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: class OpenpayGateway < Gateway - self.live_url = 'https://api.openpay.mx/v1/' - self.test_url = 'https://sandbox-api.openpay.mx/v1/' + class_attribute :mx_live_url, :mx_test_url + class_attribute :co_live_url, :co_test_url + + self.co_live_url = 'https://api.openpay.co/v1/' + self.co_test_url = 'https://sandbox-api.openpay.co/v1/' + self.mx_live_url = 'https://api.openpay.mx/v1/' + self.mx_test_url = 'https://sandbox-api.openpay.mx/v1/' + self.live_url = self.co_live_url + self.test_url = self.co_test_url self.supported_countries = %w(CO MX) self.supported_cardtypes = %i[visa master american_express carnet] @@ -24,6 +31,16 @@ def initialize(options = {}) super end + def gateway_url(options = {}) + country = options[:merchant_country] || @options[:merchant_country] + + if country == 'MX' + test? ? mx_test_url : mx_live_url + else + test? ? co_test_url : co_live_url + end + end + def purchase(money, creditcard, options = {}) post = create_post_for_auth_or_purchase(money, creditcard, options) commit(:post, 'charges', post, options) @@ -184,15 +201,17 @@ def commit(method, resource, parameters, options = {}) response = http_request(method, resource, parameters, options) success = !error?(response) - Response.new(success, + Response.new( + success, (success ? response['error_code'] : response['description']), response, test: test?, - authorization: response['id']) + authorization: response['id'] + ) end def http_request(method, resource, parameters = {}, options = {}) - url = (test? ? self.test_url : self.live_url) + @merchant_id + '/' + resource + url = gateway_url(options) + @merchant_id + '/' + resource raw_response = nil begin raw_response = ssl_request(method, url, (parameters ? parameters.to_json : nil), headers(options)) diff --git a/lib/active_merchant/billing/gateways/opp.rb b/lib/active_merchant/billing/gateways/opp.rb index 8dc6acf83ff..38d1f3b3e18 100644 --- a/lib/active_merchant/billing/gateways/opp.rb +++ b/lib/active_merchant/billing/gateways/opp.rb @@ -125,8 +125,7 @@ def initialize(options = {}) def purchase(money, payment, options = {}) # debit options[:registrationId] = payment if payment.is_a?(String) - execute_dbpa(options[:risk_workflow] ? 'PA.CP' : 'DB', - money, payment, options) + execute_dbpa(options[:risk_workflow] ? 'PA.CP' : 'DB', money, payment, options) end def authorize(money, payment, options = {}) diff --git a/lib/active_merchant/billing/gateways/optimal_payment.rb b/lib/active_merchant/billing/gateways/optimal_payment.rb index d6832282535..8ec37ff413d 100644 --- a/lib/active_merchant/billing/gateways/optimal_payment.rb +++ b/lib/active_merchant/billing/gateways/optimal_payment.rb @@ -121,11 +121,15 @@ def commit(action, money, post) txnRequest = escape_uri(xml) response = parse(ssl_post(test? ? self.test_url : self.live_url, "txnMode=#{action}&txnRequest=#{txnRequest}")) - Response.new(successful?(response), message_from(response), hash_from_xml(response), + Response.new( + successful?(response), + message_from(response), + hash_from_xml(response), test: test?, authorization: authorization_from(response), avs_result: { code: avs_result_from(response) }, - cvv_result: cvv_result_from(response)) + cvv_result: cvv_result_from(response) + ) end # The upstream is picky and so we can't use CGI.escape like we want to diff --git a/lib/active_merchant/billing/gateways/orbital.rb b/lib/active_merchant/billing/gateways/orbital.rb index a7a095fec47..5963b32e9fa 100644 --- a/lib/active_merchant/billing/gateways/orbital.rb +++ b/lib/active_merchant/billing/gateways/orbital.rb @@ -30,7 +30,7 @@ module Billing #:nodoc: class OrbitalGateway < Gateway include Empty - API_VERSION = '8.1' + API_VERSION = '9.0' POST_HEADERS = { 'MIME-Version' => '1.1', @@ -98,6 +98,7 @@ class OrbitalGateway < Gateway 'NZD' => '554', 'NOK' => '578', 'SGD' => '702', + 'ZAR' => '710', 'SEK' => '752', 'CHF' => '756', 'GBP' => '826', @@ -119,6 +120,7 @@ class OrbitalGateway < Gateway 'NZD' => '2', 'NOK' => '2', 'SGD' => '2', + 'ZAR' => '2', 'SEK' => '2', 'CHF' => '2', 'GBP' => '2', @@ -196,6 +198,7 @@ def initialize(options = {}) requires!(options, :login, :password) unless options[:ip_authentication] super @options[:merchant_id] = @options[:merchant_id].to_s + @use_secondary_url = false end # A – Authorization request @@ -235,11 +238,7 @@ def capture(money, authorization, options = {}) def refund(money, authorization, options = {}) payment_method = options[:payment_method] order = build_new_order_xml(REFUND, money, payment_method, options.merge(authorization: authorization)) do |xml| - if payment_method.is_a?(Check) - add_echeck(xml, payment_method, options) - else - add_refund_payment_source(xml, options[:currency]) - end + add_payment_source(xml, payment_method, options) xml.tag! :CustomerRefNum, options[:customer_ref_num] if @options[:customer_profiles] && options[:profile_txn] end @@ -398,6 +397,12 @@ def add_soft_descriptors(xml, descriptors) add_soft_descriptors_from_hash(xml, descriptors) if descriptors.is_a?(Hash) end + def add_payment_action_ind(xml, payment_action_ind) + return unless payment_action_ind + + xml.tag! :PaymentActionInd, payment_action_ind + end + def add_soft_descriptors_from_specialized_class(xml, soft_desc) xml.tag! :SDMerchantName, soft_desc.merchant_name if soft_desc.merchant_name xml.tag! :SDProductDescription, soft_desc.product_description if soft_desc.product_description @@ -564,7 +569,7 @@ def add_echeck(xml, check, options = {}) add_currency_fields(xml, options[:currency]) xml.tag! :BCRtNum, check.routing_number xml.tag! :CheckDDA, check.account_number if check.account_number - xml.tag! :BankAccountType, ACCOUNT_TYPE[check.account_type] if ACCOUNT_TYPE[check.account_type] + add_bank_account_type(xml, check) xml.tag! :ECPAuthMethod, options[:auth_method] if options[:auth_method] xml.tag! :BankPmtDelv, options[:payment_delivery] || 'B' xml.tag! :AVSname, (check&.name ? check.name[0..29] : nil) if get_address(options).blank? @@ -577,12 +582,6 @@ def add_credit_card(xml, credit_card, options) add_verification_value(xml, credit_card) if credit_card end - def add_refund_payment_source(xml, currency = nil) - xml.tag! :AccountNum, nil - - add_currency_fields(xml, currency) - end - def add_verification_value(xml, credit_card) return unless credit_card&.verification_value? @@ -595,7 +594,7 @@ def add_verification_value(xml, credit_card) # Null-fill this attribute OR # Do not submit the attribute at all. # - http://download.chasepaymentech.com/docs/orbital/orbital_gateway_xml_specification.pdf - xml.tag! :CardSecValInd, '1' if %w(visa master discover).include?(credit_card.brand) + xml.tag! :CardSecValInd, '1' if %w(visa discover diners_club).include?(credit_card.brand) xml.tag! :CardSecVal, credit_card.verification_value end @@ -604,6 +603,17 @@ def add_currency_fields(xml, currency) xml.tag! :CurrencyExponent, currency_exponents(currency) end + def add_bank_account_type(xml, check) + bank_account_type = + if check.account_holder_type == 'business' + 'X' + else + ACCOUNT_TYPE[check.account_type] + end + + xml.tag! :BankAccountType, bank_account_type if bank_account_type + end + def add_card_indicators(xml, options) xml.tag! :CardIndicators, options[:card_indicators] if options[:card_indicators] end @@ -752,16 +762,23 @@ def add_eci(xml, credit_card, three_d_secure) xml.tag!(:AuthenticationECIInd, eci) if eci end - def add_dpanind(xml, credit_card) + def add_dpanind(xml, credit_card, industry_type = nil) return unless credit_card.is_a?(NetworkTokenizationCreditCard) - xml.tag! :DPANInd, 'Y' + xml.tag! :DPANInd, 'Y' unless industry_type == 'RC' end - def add_digital_token_cryptogram(xml, credit_card) - return unless credit_card.is_a?(NetworkTokenizationCreditCard) + def add_digital_token_cryptogram(xml, credit_card, three_d_secure) + return unless credit_card.is_a?(NetworkTokenizationCreditCard) || three_d_secure && credit_card.brand == 'discover' - xml.tag! :DigitalTokenCryptogram, credit_card.payment_cryptogram + cryptogram = + if three_d_secure && credit_card.brand == 'discover' + three_d_secure[:cavv] + else + credit_card.payment_cryptogram + end + + xml.tag!(:DigitalTokenCryptogram, cryptogram) end #=====OTHER FIELDS===== @@ -862,18 +879,24 @@ def commit(order, message_type, retry_logic = nil, trace_number = nil) # Failover URL will be attempted in the event of a connection error response = begin + raise ConnectionError.new 'Should use secondary url', 500 if @use_secondary_url + request.call(remote_url) rescue ConnectionError request.call(remote_url(:secondary)) end - Response.new(success?(response, message_type), message_from(response), response, + Response.new( + success?(response, message_type), + message_from(response), + response, { authorization: authorization_string(response[:tx_ref_num], response[:order_id]), test: self.test?, avs_result: OrbitalGateway::AVSResult.new(response[:avs_resp_code]), cvv_result: OrbitalGateway::CVVResult.new(response[:cvv2_resp_code]) - }) + } + ) end def remote_url(url = :primary) @@ -942,6 +965,7 @@ def build_new_auth_purchase_order(action, money, payment_source, options) def build_new_order_xml(action, money, payment_source, parameters = {}) requires!(parameters, :order_id) + @use_secondary_url = parameters[:use_secondary_url] if parameters[:use_secondary_url] xml = xml_envelope xml.tag! :Request do xml.tag! :NewOrder do @@ -969,16 +993,17 @@ def build_new_order_xml(action, money, payment_source, parameters = {}) # CustomerAni, AVSPhoneType and AVSDestPhoneType could be added here. add_soft_descriptors(xml, parameters[:soft_descriptors]) - add_dpanind(xml, payment_source) + add_payment_action_ind(xml, parameters[:payment_action_ind]) + add_dpanind(xml, payment_source, parameters[:industry_type]) add_aevv(xml, payment_source, three_d_secure) - add_digital_token_cryptogram(xml, payment_source) + add_digital_token_cryptogram(xml, payment_source, three_d_secure) xml.tag! :ECPSameDayInd, parameters[:same_day] if parameters[:same_day] && payment_source.is_a?(Check) set_recurring_ind(xml, parameters) # Append Transaction Reference Number at the end for Refund transactions - add_tx_ref_num(xml, parameters[:authorization]) if action == REFUND + add_tx_ref_num(xml, parameters[:authorization]) if action == REFUND && payment_source.nil? add_level2_purchase(xml, parameters) add_level3_purchase(xml, parameters) diff --git a/lib/active_merchant/billing/gateways/pac_net_raven.rb b/lib/active_merchant/billing/gateways/pac_net_raven.rb index 2cb24b07107..56ab23cc774 100644 --- a/lib/active_merchant/billing/gateways/pac_net_raven.rb +++ b/lib/active_merchant/billing/gateways/pac_net_raven.rb @@ -121,7 +121,10 @@ def commit(action, money, parameters) test_mode = test? || message =~ /TESTMODE/ - Response.new(success?(response), message, response, + Response.new( + success?(response), + message, + response, test: test_mode, authorization: response['TrackingNumber'], fraud_review: fraud_review?(response), @@ -129,7 +132,8 @@ def commit(action, money, parameters) postal_match: AVS_POSTAL_CODES[response['AVSPostalResponseCode']], street_match: AVS_ADDRESS_CODES[response['AVSAddressResponseCode']] }, - cvv_result: CVV2_CODES[response['CVV2ResponseCode']]) + cvv_result: CVV2_CODES[response['CVV2ResponseCode']] + ) end def url(action) diff --git a/lib/active_merchant/billing/gateways/pay_gate_xml.rb b/lib/active_merchant/billing/gateways/pay_gate_xml.rb index 7d97405afac..a39c79f33b9 100644 --- a/lib/active_merchant/billing/gateways/pay_gate_xml.rb +++ b/lib/active_merchant/billing/gateways/pay_gate_xml.rb @@ -264,9 +264,13 @@ def parse(action, body) def commit(action, request, authorization = nil) response = parse(action, ssl_post(self.live_url, request)) - Response.new(successful?(response), message_from(response), response, + Response.new( + successful?(response), + message_from(response), + response, test: test?, - authorization: authorization || response[:tid]) + authorization: authorization || response[:tid] + ) end def message_from(response) diff --git a/lib/active_merchant/billing/gateways/pay_hub.rb b/lib/active_merchant/billing/gateways/pay_hub.rb index 6f66cefaac1..b6dbb6cb695 100644 --- a/lib/active_merchant/billing/gateways/pay_hub.rb +++ b/lib/active_merchant/billing/gateways/pay_hub.rb @@ -182,14 +182,16 @@ def commit(post) response = json_error(raw_response) end - Response.new(success, + Response.new( + success, response_message(response), response, test: test?, avs_result: { code: response['AVS_RESULT_CODE'] }, cvv_result: response['VERIFICATION_RESULT_CODE'], error_code: (success ? nil : STANDARD_ERROR_CODE_MAPPING[response['RESPONSE_CODE']]), - authorization: response['TRANSACTION_ID']) + authorization: response['TRANSACTION_ID'] + ) end def response_error(raw_response) diff --git a/lib/active_merchant/billing/gateways/pay_junction.rb b/lib/active_merchant/billing/gateways/pay_junction.rb index 0017dfe99ae..ce8d66fe60b 100644 --- a/lib/active_merchant/billing/gateways/pay_junction.rb +++ b/lib/active_merchant/billing/gateways/pay_junction.rb @@ -337,9 +337,13 @@ def commit(action, parameters) response = parse(ssl_post(url, post_data(action, parameters))) - Response.new(successful?(response), message_from(response), response, + Response.new( + successful?(response), + message_from(response), + response, test: test?, - authorization: response[:transaction_id] || parameters[:transaction_id]) + authorization: response[:transaction_id] || parameters[:transaction_id] + ) end def successful?(response) diff --git a/lib/active_merchant/billing/gateways/pay_secure.rb b/lib/active_merchant/billing/gateways/pay_secure.rb index db7de9bdb4b..3337fa24cf3 100644 --- a/lib/active_merchant/billing/gateways/pay_secure.rb +++ b/lib/active_merchant/billing/gateways/pay_secure.rb @@ -68,9 +68,13 @@ def add_credit_card(post, credit_card) def commit(action, money, parameters) response = parse(ssl_post(self.live_url, post_data(action, parameters))) - Response.new(successful?(response), message_from(response), response, + Response.new( + successful?(response), + message_from(response), + response, test: test_response?(response), - authorization: authorization_from(response)) + authorization: authorization_from(response) + ) end def successful?(response) diff --git a/lib/active_merchant/billing/gateways/pay_trace.rb b/lib/active_merchant/billing/gateways/pay_trace.rb index fa347886c87..53203d51f96 100644 --- a/lib/active_merchant/billing/gateways/pay_trace.rb +++ b/lib/active_merchant/billing/gateways/pay_trace.rb @@ -33,7 +33,14 @@ class PayTraceGateway < Gateway store: 'customer/create', redact: 'customer/delete', level_3_visa: 'level_three/visa', - level_3_mastercard: 'level_three/mastercard' + level_3_mastercard: 'level_three/mastercard', + ach_sale: 'checks/sale/by_account', + ach_customer_sale: 'checks/sale/by_customer', + ach_authorize: 'checks/hold/by_account', + ach_customer_authorize: 'checks/hold/by_customer', + ach_refund: 'checks/refund/by_transaction', + ach_capture: 'checks/manage/fund', + ach_void: 'checks/manage/void' } def initialize(options = {}) @@ -52,7 +59,15 @@ def purchase(money, payment_or_customer_id, options = {}) end else post = build_purchase_request(money, payment_or_customer_id, options) - post[:customer_id] ? endpoint = ENDPOINTS[:customer_id_sale] : endpoint = ENDPOINTS[:keyed_sale] + endpoint = if payment_or_customer_id.kind_of?(Check) + ENDPOINTS[:ach_sale] + elsif options[:check_transaction] + ENDPOINTS[:ach_customer_sale] + elsif post[:customer_id] + ENDPOINTS[:customer_id_sale] + else + ENDPOINTS[:keyed_sale] + end response = commit(endpoint, post) check_token_response(response, endpoint, post, options) end @@ -63,12 +78,16 @@ def authorize(money, payment_or_customer_id, options = {}) add_amount(post, money, options) if customer_id?(payment_or_customer_id) post[:customer_id] = payment_or_customer_id - endpoint = ENDPOINTS[:customer_id_auth] + endpoint = if options[:check_transaction] + ENDPOINTS[:ach_customer_authorize] + else + ENDPOINTS[:customer_id_auth] + end else add_payment(post, payment_or_customer_id) add_address(post, payment_or_customer_id, options) add_customer_data(post, options) - endpoint = ENDPOINTS[:keyed_auth] + endpoint = payment_or_customer_id.kind_of?(Check) ? ENDPOINTS[:ach_authorize] : ENDPOINTS[:keyed_auth] end response = commit(endpoint, post) check_token_response(response, endpoint, post, options) @@ -82,8 +101,13 @@ def capture(money, authorization, options = {}) end else post = build_capture_request(money, authorization, options) - response = commit(ENDPOINTS[:capture], post) - check_token_response(response, ENDPOINTS[:capture], post, options) + endpoint = if options[:check_transaction] + ENDPOINTS[:ach_capture] + else + ENDPOINTS[:capture] + end + response = commit(endpoint, post) + check_token_response(response, endpoint, post, options) end end @@ -91,17 +115,29 @@ def refund(money, authorization, options = {}) # currently only support full and partial refunds of settled transactions via a transaction ID post = {} add_amount(post, money, options) - post[:transaction_id] = authorization - response = commit(ENDPOINTS[:transaction_refund], post) - check_token_response(response, ENDPOINTS[:transaction_refund], post, options) + if options[:check_transaction] + post[:check_transaction_id] = authorization + endpoint = ENDPOINTS[:ach_refund] + else + post[:transaction_id] = authorization + endpoint = ENDPOINTS[:transaction_refund] + end + response = commit(endpoint, post) + check_token_response(response, endpoint, post, options) end def void(authorization, options = {}) post = {} - post[:transaction_id] = authorization + if options[:check_transaction] + post[:check_transaction_id] = authorization + endpoint = ENDPOINTS[:ach_void] + else + post[:transaction_id] = authorization + endpoint = ENDPOINTS[:transaction_void] + end - response = commit(ENDPOINTS[:transaction_void], post) - check_token_response(response, ENDPOINTS[:transaction_void], post, options) + response = commit(endpoint, post) + check_token_response(response, endpoint, post, options) end def verify(credit_card, options = {}) @@ -175,7 +211,11 @@ def build_purchase_request(money, payment_or_customer_id, options) def build_capture_request(money, authorization, options) post = {} - post[:transaction_id] = authorization + if options[:check_transaction] + post[:check_transaction_id] = authorization + else + post[:transaction_id] = authorization + end add_amount(post, money, options) post @@ -234,10 +274,16 @@ def add_amount(post, money, options) end def add_payment(post, payment) - post[:credit_card] = {} - post[:credit_card][:number] = payment.number - post[:credit_card][:expiration_month] = payment.month - post[:credit_card][:expiration_year] = payment.year + if payment.kind_of?(Check) + post[:check] = {} + post[:check][:account_number] = payment.account_number + post[:check][:routing_number] = payment.routing_number + else + post[:credit_card] = {} + post[:credit_card][:number] = payment.number + post[:credit_card][:expiration_month] = payment.month + post[:credit_card][:expiration_year] = payment.year + end end def add_level_3_data(post, options) @@ -380,7 +426,7 @@ def authorization_from(action, response) if action == ENDPOINTS[:store] response['customer_id'] else - response['transaction_id'] + response['transaction_id'] || response['check_transaction_id'] end end diff --git a/lib/active_merchant/billing/gateways/payeezy.rb b/lib/active_merchant/billing/gateways/payeezy.rb index c3e53f4c1d2..d8f685d2f71 100644 --- a/lib/active_merchant/billing/gateways/payeezy.rb +++ b/lib/active_merchant/billing/gateways/payeezy.rb @@ -41,6 +41,7 @@ def purchase(amount, payment_method, options = {}) add_soft_descriptors(params, options) add_level2_data(params, options) add_stored_credentials(params, options) + add_external_three_ds(params, payment_method, options) commit(params, options) end @@ -56,6 +57,7 @@ def authorize(amount, payment_method, options = {}) add_soft_descriptors(params, options) add_level2_data(params, options) add_stored_credentials(params, options) + add_external_three_ds(params, payment_method, options) commit(params, options) end @@ -125,6 +127,7 @@ def scrub(transcript) gsub(%r((Apikey: )(\w|-)+), '\1[FILTERED]'). gsub(%r((\\?"card_number\\?":\\?")\d+), '\1[FILTERED]'). gsub(%r((\\?"cvv\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r((\\?"cvv\\?":\\?)\d+), '\1[FILTERED]'). gsub(%r((\\?"account_number\\?":\\?")\d+), '\1[FILTERED]'). gsub(%r((\\?"routing_number\\?":\\?")\d+), '\1[FILTERED]'). gsub(%r((\\?card_number=)\d+(&?)), '\1[FILTERED]'). @@ -132,11 +135,33 @@ def scrub(transcript) gsub(%r((\\?apikey=)\w+(&?)), '\1[FILTERED]'). gsub(%r{(\\?"credit_card\.card_number\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]'). gsub(%r{(\\?"credit_card\.cvv\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]'). - gsub(%r{(\\?"apikey\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]') + gsub(%r{(\\?"apikey\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]'). + gsub(%r{(\\?"cavv\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]'). + gsub(%r{(\\?"xid\\?":)(\\?"[^"]+\\?")}, '\1[FILTERED]') end private + def add_external_three_ds(params, payment_method, options) + return unless three_ds = options[:three_d_secure] + + params[:'3DS'] = { + program_protocol: three_ds[:version][0], + directory_server_transaction_id: three_ds[:ds_transaction_id], + cardholder_name: payment_method.name, + card_number: payment_method.number, + exp_date: format_exp_date(payment_method.month, payment_method.year), + cvv: payment_method.verification_value, + xid: three_ds[:acs_transaction_id], + cavv: three_ds[:cavv], + wallet_provider_id: 'NO_WALLET', + type: 'D' + }.compact + + params[:eci_indicator] = options[:three_d_secure][:eci] + params[:method] = '3DS' + end + def add_invoice(params, options) params[:merchant_ref] = options[:order_id] end @@ -152,6 +177,8 @@ def amount_from_authorization(authorization) def add_authorization_info(params, authorization, options = {}) transaction_id, transaction_tag, method, = authorization.split('|') params[:method] = method == 'token' ? 'credit_card' : method + # If the previous transaction `method` value was 3DS, it needs to be set to `credit_card` on follow up transactions + params[:method] = 'credit_card' if method == '3DS' if options[:reversal_id] params[:reversal_id] = options[:reversal_id] @@ -178,6 +205,8 @@ def add_payment_method(params, payment_method, options) add_echeck(params, payment_method, options) elsif payment_method.is_a? String add_token(params, payment_method, options) + elsif payment_method.is_a? NetworkTokenizationCreditCard + add_network_tokenization(params, payment_method, options) else add_creditcard(params, payment_method) end @@ -190,7 +219,7 @@ def add_echeck(params, echeck, options) tele_check[:check_type] = 'P' tele_check[:routing_number] = echeck.routing_number tele_check[:account_number] = echeck.account_number - tele_check[:accountholder_name] = "#{echeck.first_name} #{echeck.last_name}" + tele_check[:accountholder_name] = name_from_payment_method(echeck) tele_check[:customer_id_type] = options[:customer_id_type] if options[:customer_id_type] tele_check[:customer_id_number] = options[:customer_id_number] if options[:customer_id_number] tele_check[:client_email] = options[:client_email] if options[:client_email] @@ -233,10 +262,37 @@ def add_card_data(payment_method) card end + def add_network_tokenization(params, payment_method, options) + nt_card = {} + nt_card[:type] = 'D' + nt_card[:cardholder_name] = name_from_payment_method(payment_method) || name_from_address(options) + nt_card[:card_number] = payment_method.number + nt_card[:exp_date] = format_exp_date(payment_method.month, payment_method.year) + nt_card[:cvv] = payment_method.verification_value + nt_card[:xid] = payment_method.payment_cryptogram unless payment_method.payment_cryptogram.empty? || payment_method.brand.include?('american_express') + nt_card[:cavv] = payment_method.payment_cryptogram unless payment_method.payment_cryptogram.empty? + nt_card[:wallet_provider_id] = 'APPLE_PAY' + + params['3DS'] = nt_card + params[:method] = '3DS' + params[:eci_indicator] = payment_method.eci.nil? ? '5' : payment_method.eci + end + def format_exp_date(month, year) "#{format(month, :two_digits)}#{format(year, :two_digits)}" end + def name_from_address(options) + return unless address = options[:billing_address] + return address[:name] if address[:name] + end + + def name_from_payment_method(payment_method) + return unless payment_method.first_name && payment_method.last_name + + return "#{payment_method.first_name} #{payment_method.last_name}" + end + def add_address(params, options) address = options[:billing_address] return unless address @@ -279,8 +335,7 @@ def add_stored_credentials(params, options) end def original_transaction_id(options) - return options[:cardbrand_original_transaction_id] if options[:cardbrand_original_transaction_id] - return options[:stored_credential][:network_transaction_id] if options.dig(:stored_credential, :network_transaction_id) + return options[:cardbrand_original_transaction_id] || options.dig(:stored_credential, :network_transaction_id) end def initiator(options) diff --git a/lib/active_merchant/billing/gateways/payex.rb b/lib/active_merchant/billing/gateways/payex.rb index 3d63b8957a0..c1449672e4b 100644 --- a/lib/active_merchant/billing/gateways/payex.rb +++ b/lib/active_merchant/billing/gateways/payex.rb @@ -385,11 +385,13 @@ def commit(soap_action, request) 'Content-Length' => request.size.to_s } response = parse(ssl_post(url, request, headers)) - Response.new(success?(response), + Response.new( + success?(response), message_from(response), response, test: test?, - authorization: build_authorization(response)) + authorization: build_authorization(response) + ) end def build_authorization(response) diff --git a/lib/active_merchant/billing/gateways/payment_express.rb b/lib/active_merchant/billing/gateways/payment_express.rb index 81369f2b432..51517b9277d 100644 --- a/lib/active_merchant/billing/gateways/payment_express.rb +++ b/lib/active_merchant/billing/gateways/payment_express.rb @@ -20,8 +20,8 @@ class PaymentExpressGateway < Gateway self.homepage_url = 'https://www.windcave.com/' self.display_name = 'Windcave (formerly PaymentExpress)' - self.live_url = 'https://sec.paymentexpress.com/pxpost.aspx' - self.test_url = 'https://uat.paymentexpress.com/pxpost.aspx' + self.live_url = 'https://sec.windcave.com/pxpost.aspx' + self.test_url = 'https://uat.windcave.com/pxpost.aspx' APPROVED = '1' @@ -306,9 +306,13 @@ def commit(action, request) response = parse(ssl_post(url, request.to_s)) # Return a response - PaymentExpressResponse.new(response[:success] == APPROVED, message_from(response), response, + PaymentExpressResponse.new( + response[:success] == APPROVED, + message_from(response), + response, test: response[:test_mode] == '1', - authorization: authorization_from(action, response)) + authorization: authorization_from(action, response) + ) end # Response XML documentation: http://www.paymentexpress.com/technical_resources/ecommerce_nonhosted/pxpost.html#XMLTxnOutput diff --git a/lib/active_merchant/billing/gateways/paymentez.rb b/lib/active_merchant/billing/gateways/paymentez.rb index 3e71b1e1989..cc033bcddb3 100644 --- a/lib/active_merchant/billing/gateways/paymentez.rb +++ b/lib/active_merchant/billing/gateways/paymentez.rb @@ -137,6 +137,10 @@ def unstore(identification, options = {}) commit_card('delete', post) end + def inquire(authorization, options = {}) + commit_transaction('inquire', authorization) + end + def supports_scrubbing? true end @@ -232,12 +236,20 @@ def parse(body) end def commit_raw(object, action, parameters) - url = "#{(test? ? test_url : live_url)}#{object}/#{action}" - - begin - raw_response = ssl_post(url, post_data(parameters), headers) - rescue ResponseError => e - raw_response = e.response.body + if action == 'inquire' + url = "#{(test? ? test_url : live_url)}#{object}/#{parameters}" + begin + raw_response = ssl_get(url, headers) + rescue ResponseError => e + raw_response = e.response.body + end + else + url = "#{(test? ? test_url : live_url)}#{object}/#{action}" + begin + raw_response = ssl_post(url, post_data(parameters), headers) + rescue ResponseError => e + raw_response = e.response.body + end end begin diff --git a/lib/active_merchant/billing/gateways/paypal/paypal_express_response.rb b/lib/active_merchant/billing/gateways/paypal/paypal_express_response.rb index eeb38da4117..d5f2b00a85e 100644 --- a/lib/active_merchant/billing/gateways/paypal/paypal_express_response.rb +++ b/lib/active_merchant/billing/gateways/paypal/paypal_express_response.rb @@ -13,6 +13,10 @@ def details (@params['PaymentDetails']||{}) end + def checkout_status + (@params['CheckoutStatus']||{}) + end + def name payer = (info['PayerName']||{}) [payer['FirstName'], payer['MiddleName'], payer['LastName']].compact.join(' ') diff --git a/lib/active_merchant/billing/gateways/paypal_express.rb b/lib/active_merchant/billing/gateways/paypal_express.rb index aaa65cec7e2..597cfeda9c3 100644 --- a/lib/active_merchant/billing/gateways/paypal_express.rb +++ b/lib/active_merchant/billing/gateways/paypal_express.rb @@ -108,6 +108,7 @@ def build_sale_or_authorization_request(action, money, options) xml.tag! 'n2:PaymentAction', action xml.tag! 'n2:Token', options[:token] xml.tag! 'n2:PayerID', options[:payer_id] + xml.tag! 'n2:MsgSubID', options[:idempotency_key] if options[:idempotency_key] add_payment_details(xml, money, currency_code, options) end end @@ -251,6 +252,7 @@ def build_reference_transaction_request(action, money, options) add_payment_details(xml, money, currency_code, options) xml.tag! 'n2:IPAddress', options[:ip] xml.tag! 'n2:MerchantSessionId', options[:merchant_session_id] if options[:merchant_session_id].present? + xml.tag! 'n2:MsgSubID', options[:idempotency_key] if options[:idempotency_key] end end end diff --git a/lib/active_merchant/billing/gateways/paysafe.rb b/lib/active_merchant/billing/gateways/paysafe.rb index 79244c009dd..a7cff9fe813 100644 --- a/lib/active_merchant/billing/gateways/paysafe.rb +++ b/lib/active_merchant/billing/gateways/paysafe.rb @@ -17,14 +17,8 @@ def initialize(options = {}) def purchase(money, payment, options = {}) post = {} - add_invoice(post, money, options) - add_payment(post, payment) - add_billing_address(post, options) - add_merchant_details(post, options) + add_auth_purchase_params(post, money, payment, options) add_airline_travel_details(post, options) - add_customer_data(post, payment, options) unless payment.is_a?(String) - add_three_d_secure(post, payment, options) if options[:three_d_secure] - add_stored_credential(post, options) if options[:stored_credential] add_split_pay_details(post, options) post[:settleWithAuth] = true @@ -33,13 +27,7 @@ def purchase(money, payment, options = {}) def authorize(money, payment, options = {}) post = {} - add_invoice(post, money, options) - add_payment(post, payment) - add_billing_address(post, options) - add_merchant_details(post, options) - add_customer_data(post, payment, options) unless payment.is_a?(String) - add_three_d_secure(post, payment, options) if options[:three_d_secure] - add_stored_credential(post, options) if options[:stored_credential] + add_auth_purchase_params(post, money, payment, options) commit(:post, 'auths', post, options) end @@ -111,6 +99,17 @@ def scrub(transcript) private + def add_auth_purchase_params(post, money, payment, options) + add_invoice(post, money, options) + add_payment(post, payment) + add_billing_address(post, options) + add_merchant_details(post, options) + add_customer_data(post, payment, options) unless payment.is_a?(String) + add_three_d_secure(post, payment, options) if options[:three_d_secure] + add_stored_credential(post, options) if options[:stored_credential] + add_funding_transaction(post, options) + end + # Customer data can be included in transactions where the payment method is a credit card # but should not be sent when the payment method is a token def add_customer_data(post, creditcard, options) @@ -125,12 +124,13 @@ def add_billing_address(post, options) return unless address = options[:billing_address] || options[:address] post[:billingDetails] = {} - post[:billingDetails][:street] = address[:address1] - post[:billingDetails][:city] = address[:city] - post[:billingDetails][:state] = address[:state] + post[:billingDetails][:street] = truncate(address[:address1], 50) + post[:billingDetails][:street2] = truncate(address[:address2], 50) + post[:billingDetails][:city] = truncate(address[:city], 40) + post[:billingDetails][:state] = truncate(address[:state], 40) post[:billingDetails][:country] = address[:country] - post[:billingDetails][:zip] = address[:zip] - post[:billingDetails][:phone] = address[:phone] + post[:billingDetails][:zip] = truncate(address[:zip], 10) + post[:billingDetails][:phone] = truncate(address[:phone], 40) end # The add_address_for_vaulting method is applicable to the store method, as the APIs address @@ -139,12 +139,12 @@ def add_address_for_vaulting(post, options) return unless address = options[:billing_address] || options[:address] post[:card][:billingAddress] = {} - post[:card][:billingAddress][:street] = address[:address1] - post[:card][:billingAddress][:street2] = address[:address2] - post[:card][:billingAddress][:city] = address[:city] - post[:card][:billingAddress][:zip] = address[:zip] + post[:card][:billingAddress][:street] = truncate(address[:address1], 50) + post[:card][:billingAddress][:street2] = truncate(address[:address2], 50) + post[:card][:billingAddress][:city] = truncate(address[:city], 40) + post[:card][:billingAddress][:zip] = truncate(address[:zip], 10) post[:card][:billingAddress][:country] = address[:country] - post[:card][:billingAddress][:state] = address[:state] if address[:state] + post[:card][:billingAddress][:state] = truncate(address[:state], 40) if address[:state] end # This data is specific to creating a profile at the gateway's vault level @@ -286,6 +286,15 @@ def add_split_pay_details(post, options) post[:splitpay] = split_pay end + def add_funding_transaction(post, options) + return unless options[:funding_transaction] + + post[:fundingTransaction] = {} + post[:fundingTransaction][:type] = options[:funding_transaction] + post[:profile] ||= {} + post[:profile][:merchantCustomerId] = options[:customer_id] || SecureRandom.hex(12) + end + def add_stored_credential(post, options) return unless options[:stored_credential] @@ -393,7 +402,7 @@ def get_id_from_store_auth(authorization) def post_data(parameters = {}, options = {}) return unless parameters.present? - parameters[:merchantRefNum] = options[:merchant_ref_num] || SecureRandom.hex(16).to_s + parameters[:merchantRefNum] = options[:merchant_ref_num] || options[:order_id] || SecureRandom.hex(16).to_s parameters.to_json end diff --git a/lib/active_merchant/billing/gateways/payscout.rb b/lib/active_merchant/billing/gateways/payscout.rb index 4c2f418a8c8..dec04a926f6 100644 --- a/lib/active_merchant/billing/gateways/payscout.rb +++ b/lib/active_merchant/billing/gateways/payscout.rb @@ -115,12 +115,16 @@ def commit(action, money, parameters) message = message_from(response) test_mode = (test? || message =~ /TESTMODE/) - Response.new(success?(response), message, response, + Response.new( + success?(response), + message, + response, test: test_mode, authorization: response['transactionid'], fraud_review: fraud_review?(response), avs_result: { code: response['avsresponse'] }, - cvv_result: response['cvvresponse']) + cvv_result: response['cvvresponse'] + ) end def message_from(response) diff --git a/lib/active_merchant/billing/gateways/paystation.rb b/lib/active_merchant/billing/gateways/paystation.rb index e7557ce5419..4ec37cff955 100644 --- a/lib/active_merchant/billing/gateways/paystation.rb +++ b/lib/active_merchant/billing/gateways/paystation.rb @@ -177,9 +177,13 @@ def commit(post) response = parse(data) message = message_from(response) - PaystationResponse.new(success?(response), message, response, + PaystationResponse.new( + success?(response), + message, + response, test: (response[:tm]&.casecmp('t')&.zero?), - authorization: response[:paystation_transaction_id]) + authorization: response[:paystation_transaction_id] + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/payu_latam.rb b/lib/active_merchant/billing/gateways/payu_latam.rb index ec8a798a479..3ac30eec018 100644 --- a/lib/active_merchant/billing/gateways/payu_latam.rb +++ b/lib/active_merchant/billing/gateways/payu_latam.rb @@ -313,6 +313,9 @@ def add_process_without_cvv2(payment_method, options) def add_extra_parameters(post, options) extra_parameters = {} extra_parameters[:INSTALLMENTS_NUMBER] = options[:installments_number] || 1 + extra_parameters[:EXTRA1] = options[:extra_1] if options[:extra_1] + extra_parameters[:EXTRA2] = options[:extra_2] if options[:extra_2] + extra_parameters[:EXTRA3] = options[:extra_3] if options[:extra_3] post[:transaction][:extraParameters] = extra_parameters end @@ -452,7 +455,7 @@ def error_from(action, response) when 'verify_credentials' response['error'] || 'FAILED' else - response['transactionResponse']['errorCode'] || response['transactionResponse']['responseCode'] if response['transactionResponse'] + response['transactionResponse']['paymentNetworkResponseCode'] || response['transactionResponse']['errorCode'] if response['transactionResponse'] end end diff --git a/lib/active_merchant/billing/gateways/payway.rb b/lib/active_merchant/billing/gateways/payway.rb index e5980c8ce04..c48373ea608 100644 --- a/lib/active_merchant/billing/gateways/payway.rb +++ b/lib/active_merchant/billing/gateways/payway.rb @@ -192,9 +192,13 @@ def commit(action, post) success = (params[:summary_code] ? (params[:summary_code] == '0') : (params[:response_code] == '00')) - Response.new(success, message, params, + Response.new( + success, + message, + params, test: (@options[:merchant].to_s == 'TEST'), - authorization: post[:order_number]) + authorization: post[:order_number] + ) rescue ActiveMerchant::ResponseError => e raise unless e.response.code == '403' diff --git a/lib/active_merchant/billing/gateways/payway_dot_com.rb b/lib/active_merchant/billing/gateways/payway_dot_com.rb index 06f6d919360..995889b53bc 100644 --- a/lib/active_merchant/billing/gateways/payway_dot_com.rb +++ b/lib/active_merchant/billing/gateways/payway_dot_com.rb @@ -2,7 +2,7 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: class PaywayDotComGateway < Gateway self.test_url = 'https://paywaywsdev.com/PaywayWS/Payment/CreditCard' - self.live_url = 'https://paywayws.com/PaywayWS/Payment/CreditCard' + self.live_url = 'https://paywayws.net/PaywayWS/Payment/CreditCard' self.supported_countries = %w[US CA] self.default_currency = 'USD' @@ -48,7 +48,7 @@ class PaywayDotComGateway < Gateway 'I5' => 'M', # +4 and Address Match 'I6' => 'W', # +4 Match 'I7' => 'A', # Address Match - 'I8' => 'C', # No Match + 'I8' => 'C' # No Match } PAYWAY_WS_SUCCESS = '5000' diff --git a/lib/active_merchant/billing/gateways/plexo.rb b/lib/active_merchant/billing/gateways/plexo.rb new file mode 100644 index 00000000000..f4558e1c4df --- /dev/null +++ b/lib/active_merchant/billing/gateways/plexo.rb @@ -0,0 +1,308 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class PlexoGateway < Gateway + self.test_url = 'https://api.testing.plexo.com.uy/v1/payments' + self.live_url = 'https://api.plexo.com.uy/v1/payments' + + self.supported_countries = ['UY'] + self.default_currency = 'UYU' + self.supported_cardtypes = %i[visa master american_express discover passcard edenred anda tarjeta-d] + + self.homepage_url = 'https://www.plexo.com.uy' + self.display_name = 'Plexo' + + APPENDED_URLS = %w(captures refunds cancellations verify) + AMOUNT_IN_RESPONSE = %w(authonly purchase /verify) + APPROVED_STATUS = %w(approved authorized) + + def initialize(options = {}) + requires!(options, :client_id, :api_key) + @credentials = options + super + end + + def purchase(money, payment, options = {}) + post = {} + build_auth_purchase_request(money, post, payment, options) + + commit('purchase', post, options) + end + + def authorize(money, payment, options = {}) + post = {} + build_auth_purchase_request(money, post, payment, options) + add_capture_type(post, options) + + commit('authonly', post, options) + end + + def capture(money, authorization, options = {}) + post = {} + post[:ReferenceId] = options[:reference_id] || generate_unique_id + post[:Amount] = amount(money) + + commit("/#{authorization}/captures", post, options) + end + + def refund(money, authorization, options = {}) + post = {} + post[:ReferenceId] = options[:reference_id] || generate_unique_id + post[:Type] = options[:refund_type] || 'refund' + post[:Description] = options[:description] + post[:Reason] = options[:reason] + post[:Amount] = amount(money) + + commit("/#{authorization}/refunds", post, options) + end + + def void(authorization, options = {}) + post = {} + post[:ReferenceId] = options[:reference_id] || generate_unique_id + post[:Description] = options[:description] + post[:Reason] = options[:reason] + + commit("/#{authorization}/cancellations", post, options) + end + + def verify(credit_card, options = {}) + post = {} + post[:ReferenceId] = options[:reference_id] || generate_unique_id + post[:MerchantId] = options[:merchant_id] || @credentials[:merchant_id] + post[:StatementDescriptor] = options[:statement_descriptor] if options[:statement_descriptor] + post[:CustomerId] = options[:customer_id] if options[:customer_id] + money = options[:verify_amount].to_i || 100 + + add_payment_method(post, credit_card, options) + add_metadata(post, options[:metadata]) + add_amount(money, post, options) + add_browser_details(post, options) + + commit('/verify', post, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r(("Number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("Cvc\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("InvoiceNumber\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("MerchantId\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]') + end + + private + + def encoded_credentials + Base64.encode64("#{@credentials[:client_id]}:#{@credentials[:api_key]}").delete("\n") + end + + def build_auth_purchase_request(money, post, payment, options) + post[:ReferenceId] = options[:reference_id] || generate_unique_id + post[:MerchantId] = options[:merchant_id] || @credentials[:merchant_id] + post[:Installments] = options[:installments] if options[:installments] + post[:StatementDescriptor] = options[:statement_descriptor] if options[:statement_descriptor] + post[:CustomerId] = options[:customer_id] if options[:customer_id] + + add_payment_method(post, payment, options) + add_items(post, options[:items]) + add_metadata(post, options[:metadata]) + add_amount(money, post, options) + add_browser_details(post, options) + end + + def header(parameters = {}) + { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{encoded_credentials}" + } + end + + def add_capture_type(post, options) + post[:Capture] = {} + post[:Capture][:Method] = options.dig(:capture_type, :method) || 'manual' + end + + def add_items(post, items) + return unless items&.kind_of?(Array) + + post[:Items] = [] + + items.each do |option_item| + item = {} + item[:ReferenceId] = option_item[:reference_id] || generate_unique_id + item[:Name] = option_item[:name] if option_item[:name] + item[:Description] = option_item[:description] if option_item[:description] + item[:Quantity] = option_item[:quantity] if option_item[:quantity] + item[:Price] = option_item[:price] if option_item[:price] + item[:Discount] = option_item[:discount] if option_item[:discount] + + post[:Items].append(item) + end + end + + def add_metadata(post, metadata) + return unless metadata&.kind_of?(Hash) + + metadata.transform_keys! { |key| key.to_s.camelize.to_sym } + post[:Metadata] = metadata + end + + def add_amount(money, post, amount_options) + post[:Amount] = {} + + post[:Amount][:Currency] = amount_options[:currency] || self.default_currency + post[:Amount][:Total] = amount(money) + post[:Amount][:Details] = {} + add_amount_details(post[:Amount][:Details], amount_options[:amount_details]) if amount_options[:amount_details] + end + + def add_amount_details(amount_details, options) + return unless options + + amount_details[:TaxedAmount] = options[:taxed_amount] if options[:taxed_amount] + amount_details[:TipAmount] = options[:tip_amount] if options[:tip_amount] + amount_details[:DiscountAmount] = options[:discount_amount] if options[:discount_amount] + amount_details[:TaxableAmount] = options[:taxable_amount] if options[:taxable_amount] + add_tax(amount_details, options[:tax]) + end + + def add_tax(post, tax) + return unless tax + + post[:Tax] = {} + post[:Tax][:Type] = tax[:type] if tax[:type] + post[:Tax][:Amount] = tax[:amount] if tax[:amount] + post[:Tax][:Rate] = tax[:rate] if tax[:rate] + end + + def add_browser_details(post, browser_details) + return unless browser_details + + post[:BrowserDetails] = {} + post[:BrowserDetails][:DeviceFingerprint] = browser_details[:finger_print] if browser_details[:finger_print] + post[:BrowserDetails][:IpAddress] = browser_details[:ip] if browser_details[:ip] + end + + def add_payment_method(post, payment, options) + post[:paymentMethod] = {} + + if payment&.is_a?(CreditCard) + post[:paymentMethod][:type] = 'card' + post[:paymentMethod][:Card] = {} + post[:paymentMethod][:Card][:Number] = payment.number + post[:paymentMethod][:Card][:ExpMonth] = format(payment.month, :two_digits) if payment.month + post[:paymentMethod][:Card][:ExpYear] = format(payment.year, :two_digits) if payment.year + post[:paymentMethod][:Card][:Cvc] = payment.verification_value if payment.verification_value + + add_card_holder(post[:paymentMethod][:Card], payment, options) + end + end + + def add_card_holder(card, payment, options) + requires!(options, :email) + + cardholder = {} + cardholder[:FirstName] = payment.first_name if payment.first_name + cardholder[:LastName] = payment.last_name if payment.last_name + cardholder[:Email] = options[:email] + cardholder[:Birthdate] = options[:cardholder_birthdate] if options[:cardholder_birthdate] + cardholder[:Identification] = {} + cardholder[:Identification][:Type] = options[:identification_type] if options[:identification_type] + cardholder[:Identification][:Value] = options[:identification_value] if options[:identification_value] + add_billing_address(cardholder, options) + + card[:Cardholder] = cardholder + end + + def add_billing_address(cardholder, options) + return unless address = options[:billing_address] + + cardholder[:BillingAddress] = {} + cardholder[:BillingAddress][:City] = address[:city] + cardholder[:BillingAddress][:Country] = address[:country] + cardholder[:BillingAddress][:Line1] = address[:address1] + cardholder[:BillingAddress][:Line2] = address[:address2] + cardholder[:BillingAddress][:PostalCode] = address[:zip] + cardholder[:BillingAddress][:State] = address[:state] + end + + def parse(body) + return {} if body == '' + + JSON.parse(body) + end + + def build_url(action, base) + url = base + url += action if APPENDED_URLS.any? { |key| action.include?(key) } + url + end + + def get_authorization_from_url(url) + url.split('/')[1] + end + + def reorder_amount_fields(response) + return response unless response['amount'] + + amount_obj = response['amount'] + response['amount'] = amount_obj['total'].to_i if amount_obj['total'] + response['currency'] = amount_obj['currency'] if amount_obj['currency'] + response['amount_details'] = amount_obj['details'] if amount_obj['details'] + response + end + + def commit(action, parameters, options = {}) + base_url = (test? ? test_url : live_url) + url = build_url(action, base_url) + response = parse(ssl_post(url, parameters.to_json, header(options))) + response = reorder_amount_fields(response) if AMOUNT_IN_RESPONSE.include?(action) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response, action), + test: test?, + error_code: error_code_from(response) + ) + end + + def handle_response(response) + case response.code.to_i + when 200...300, 400, 401 + response.body + else + raise ResponseError.new(response) + end + end + + def success_from(response) + APPROVED_STATUS.include?(response['status']) + end + + def message_from(response) + response = response['transactions']&.first if response['transactions']&.is_a?(Array) + response['resultMessage'] || response['message'] + end + + def authorization_from(response, action = nil) + if action.include?('captures') + get_authorization_from_url(action) + else + response['id'] + end + end + + def error_code_from(response) + return if success_from(response) + + response = response['transactions']&.first if response['transactions']&.is_a?(Array) + response['resultCode'] || response['status'] + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/plugnpay.rb b/lib/active_merchant/billing/gateways/plugnpay.rb index e7343efde03..e383c0d4ef7 100644 --- a/lib/active_merchant/billing/gateways/plugnpay.rb +++ b/lib/active_merchant/billing/gateways/plugnpay.rb @@ -178,11 +178,15 @@ def commit(action, post) success = SUCCESS_CODES.include?(response[:finalstatus]) message = success ? 'Success' : message_from(response) - Response.new(success, message, response, + Response.new( + success, + message, + response, test: test?, authorization: response[:orderid], avs_result: { code: response[:avs_code] }, - cvv_result: response[:cvvresp]) + cvv_result: response[:cvvresp] + ) end def parse(body) diff --git a/lib/active_merchant/billing/gateways/priority.rb b/lib/active_merchant/billing/gateways/priority.rb index 759feec26e1..cddde41395c 100644 --- a/lib/active_merchant/billing/gateways/priority.rb +++ b/lib/active_merchant/billing/gateways/priority.rb @@ -27,12 +27,12 @@ class PriorityGateway < Gateway self.display_name = 'Priority' def initialize(options = {}) - requires!(options, :merchant_id, :key, :secret) + requires!(options, :merchant_id, :api_key, :secret) super end def basic_auth - Base64.strict_encode64("#{@options[:key]}:#{@options[:secret]}") + Base64.strict_encode64("#{@options[:api_key]}:#{@options[:secret]}") end def request_headers @@ -72,6 +72,18 @@ def authorize(amount, credit_card, options = {}) commit('purchase', params: params) end + def credit(amount, credit_card, options = {}) + params = {} + params['authOnly'] = false + params['isSettleFunds'] = true + amount = -amount + + add_merchant_id(params) + add_amount(params, amount, options) + add_credit_params(params, credit_card, options) + commit('credit', params: params) + end + def refund(amount, authorization, options = {}) params = {} add_merchant_id(params) @@ -147,6 +159,12 @@ def add_auth_purchase_params(params, credit_card, options) add_additional_data(params, options) end + def add_credit_params(params, credit_card, options) + add_replay_id(params, options) + add_credit_card(params, credit_card, 'purchase', options) + add_additional_data(params, options) + end + def add_replay_id(params, options) params['replayId'] = options[:replay_id] if options[:replay_id] end @@ -157,13 +175,12 @@ def add_credit_card(params, credit_card, action, options) card_details = {} card_details['expiryMonth'] = format(credit_card.month, :two_digits).to_s card_details['expiryYear'] = format(credit_card.year, :two_digits).to_s - card_details['expiryDate'] = exp_date(credit_card) card_details['cardType'] = credit_card.brand card_details['last4'] = credit_card.last_digits - card_details['cvv'] = credit_card.verification_value + card_details['cvv'] = credit_card.verification_value unless credit_card.verification_value.nil? card_details['number'] = credit_card.number card_details['avsStreet'] = options[:billing_address][:address1] if options[:billing_address] - card_details['avsZip'] = options[:billing_address][:zip] if options[:billing_address] + card_details['avsZip'] = options[:billing_address][:zip] if !options[:billing_address].nil? && !options[:billing_address][:zip].nil? params['cardAccount'] = card_details end @@ -181,11 +198,17 @@ def add_additional_data(params, options) params['shouldGetCreditCardLevel'] = options[:should_get_credit_card_level] if options[:should_get_credit_card_level] params['source'] = options[:source] if options[:source] params['invoice'] = options[:invoice] if options[:invoice] + params['isTicket'] = options[:is_ticket] if options[:is_ticket] + params['shouldVaultCard'] = options[:should_vault_card] if options[:should_vault_card] + params['sourceZip'] = options[:source_zip] if options[:source_zip] + params['authCode'] = options[:auth_code] if options[:auth_code] + params['achIndicator'] = options[:ach_indicator] if options[:ach_indicator] + params['bankAccount'] = options[:bank_account] if options[:bank_account] + params['meta'] = options[:meta] if options[:meta] end def add_pos_data(params, options) pos_data = {} - pos_data['cardholderPresence'] = options.dig(:pos_data, :cardholder_presence) || 'Ecom' pos_data['deviceAttendance'] = options.dig(:pos_data, :device_attendance) || 'HomePc' pos_data['deviceInputCapability'] = options.dig(:pos_data, :device_input_capability) || 'Unknown' diff --git a/lib/active_merchant/billing/gateways/psigate.rb b/lib/active_merchant/billing/gateways/psigate.rb index 6fc9c8905e4..c383ddd0cd9 100644 --- a/lib/active_merchant/billing/gateways/psigate.rb +++ b/lib/active_merchant/billing/gateways/psigate.rb @@ -102,11 +102,15 @@ def scrub(transcript) def commit(money, creditcard, options = {}) response = parse(ssl_post(url, post_data(money, creditcard, options))) - Response.new(successful?(response), message_from(response), response, + Response.new( + successful?(response), + message_from(response), + response, test: test?, authorization: build_authorization(response), avs_result: { code: response[:avsresult] }, - cvv_result: response[:cardidresult]) + cvv_result: response[:cardidresult] + ) end def url diff --git a/lib/active_merchant/billing/gateways/psl_card.rb b/lib/active_merchant/billing/gateways/psl_card.rb index ec688861457..11f20a2a1ad 100644 --- a/lib/active_merchant/billing/gateways/psl_card.rb +++ b/lib/active_merchant/billing/gateways/psl_card.rb @@ -259,11 +259,15 @@ def parse(body) def commit(request) response = parse(ssl_post(self.live_url, post_data(request))) - Response.new(response[:ResponseCode] == APPROVED, response[:Message], response, + Response.new( + response[:ResponseCode] == APPROVED, + response[:Message], + response, test: test?, authorization: response[:CrossReference], cvv_result: CVV_CODE[response[:AVSCV2Check]], - avs_result: { code: AVS_CODE[response[:AVSCV2Check]] }) + avs_result: { code: AVS_CODE[response[:AVSCV2Check]] } + ) end # Put the passed data into a format that can be submitted to PSL diff --git a/lib/active_merchant/billing/gateways/qbms.rb b/lib/active_merchant/billing/gateways/qbms.rb index c709905a293..354928930aa 100644 --- a/lib/active_merchant/billing/gateways/qbms.rb +++ b/lib/active_merchant/billing/gateways/qbms.rb @@ -142,12 +142,16 @@ def commit(action, money, parameters) response = parse(type, data) message = (response[:status_message] || '').strip - Response.new(success?(response), message, response, + Response.new( + success?(response), + message, + response, test: test?, authorization: response[:credit_card_trans_id], fraud_review: fraud_review?(response), avs_result: { code: avs_result(response) }, - cvv_result: cvv_result(response)) + cvv_result: cvv_result(response) + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/quantum.rb b/lib/active_merchant/billing/gateways/quantum.rb index 104148b662b..693bcdb9b7a 100644 --- a/lib/active_merchant/billing/gateways/quantum.rb +++ b/lib/active_merchant/billing/gateways/quantum.rb @@ -215,11 +215,15 @@ def commit(request, options) authorization = success ? authorization_for(response) : nil end - Response.new(success, message, response, + Response.new( + success, + message, + response, test: test?, authorization: authorization, avs_result: { code: response[:AVSResponseCode] }, - cvv_result: response[:CVV2ResponseCode]) + cvv_result: response[:CVV2ResponseCode] + ) end # Parse the SOAP response diff --git a/lib/active_merchant/billing/gateways/quickbooks.rb b/lib/active_merchant/billing/gateways/quickbooks.rb index 402637475e5..6d9f14f3445 100644 --- a/lib/active_merchant/billing/gateways/quickbooks.rb +++ b/lib/active_merchant/billing/gateways/quickbooks.rb @@ -42,10 +42,10 @@ class QuickbooksGateway < Gateway 'PMT-5001' => STANDARD_ERROR_CODE[:card_declined], # Merchant does not support given payment method # System Error - 'PMT-6000' => STANDARD_ERROR_CODE[:processing_error], # A temporary Issue prevented this request from being processed. + 'PMT-6000' => STANDARD_ERROR_CODE[:processing_error] # A temporary Issue prevented this request from being processed. } - FRAUD_WARNING_CODES = ['PMT-1000', 'PMT-1001', 'PMT-1002', 'PMT-1003'] + FRAUD_WARNING_CODES = %w(PMT-1000 PMT-1001 PMT-1002 PMT-1003) def initialize(options = {}) # Quickbooks is deprecating OAuth 1.0 on December 17, 2019. diff --git a/lib/active_merchant/billing/gateways/quickpay/quickpay_v10.rb b/lib/active_merchant/billing/gateways/quickpay/quickpay_v10.rb index 91eeeff564e..ce71535e833 100644 --- a/lib/active_merchant/billing/gateways/quickpay/quickpay_v10.rb +++ b/lib/active_merchant/billing/gateways/quickpay/quickpay_v10.rb @@ -153,9 +153,13 @@ def commit(action, params = {}) response = json_error(response) end - Response.new(success, message_from(success, response), response, + Response.new( + success, + message_from(success, response), + response, test: test?, - authorization: authorization_from(response)) + authorization: authorization_from(response) + ) end def authorization_from(response) diff --git a/lib/active_merchant/billing/gateways/quickpay/quickpay_v4to7.rb b/lib/active_merchant/billing/gateways/quickpay/quickpay_v4to7.rb index bb00258a3f6..c4daca39787 100644 --- a/lib/active_merchant/billing/gateways/quickpay/quickpay_v4to7.rb +++ b/lib/active_merchant/billing/gateways/quickpay/quickpay_v4to7.rb @@ -164,9 +164,13 @@ def add_finalize(post, options) def commit(action, params) response = parse(ssl_post(self.live_url, post_data(action, params))) - Response.new(successful?(response), message_from(response), response, + Response.new( + successful?(response), + message_from(response), + response, test: test?, - authorization: response[:transaction]) + authorization: response[:transaction] + ) end def successful?(response) diff --git a/lib/active_merchant/billing/gateways/rapyd.rb b/lib/active_merchant/billing/gateways/rapyd.rb index acb0dcb72bc..e23653f1f65 100644 --- a/lib/active_merchant/billing/gateways/rapyd.rb +++ b/lib/active_merchant/billing/gateways/rapyd.rb @@ -4,13 +4,15 @@ class RapydGateway < Gateway self.test_url = 'https://sandboxapi.rapyd.net/v1/' self.live_url = 'https://api.rapyd.net/v1/' - self.supported_countries = %w(US BR CA CL CO DO SV MX PE PT VI AU HK IN ID JP MY NZ PH SG KR TW TH VN AD AT BE BA BG HR CY CZ DK EE FI FR GE DE GI GR GL HU IS IE IL IT LV LI LT LU MK MT MD MC ME NL GB NO PL RO RU SM SK SI ZA ES SE CH TR VA) + self.supported_countries = %w(CA CL CO DO SV PE PT VI AU HK IN ID JP MY NZ PH SG KR TW TH VN AD AT BE BA BG HR CY CZ DK EE FI FR GE DE GI GR GL HU IS IE IL IT LV LI LT LU MK MT MD MC ME NL GB NO PL RO RU SM SK SI ZA ES SE CH TR VA) self.default_currency = 'USD' - self.supported_cardtypes = %i[visa master american_express discover] + self.supported_cardtypes = %i[visa master american_express discover verve] self.homepage_url = 'https://www.rapyd.net/' self.display_name = 'Rapyd Gateway' + USA_PAYMENT_METHODS = %w[us_debit_discover_card us_debit_mastercard_card us_debit_visa_card us_ach_bank] + STANDARD_ERROR_CODE_MAPPING = {} def initialize(options = {}) @@ -20,63 +22,58 @@ def initialize(options = {}) def purchase(money, payment, options = {}) post = {} - add_invoice(post, money, options) - add_payment(post, payment, options) - add_address(post, payment, options) - add_metadata(post, options) - add_ewallet(post, options) - post[:capture] = true if payment_is_card?(options) - - if payment_is_ach?(options) - MultiResponse.run do |r| - r.process { commit(:post, 'payments', post) } - post = {} - post[:token] = r.authorization - post[:param2] = r.params.dig('data', 'original_amount').to_s - r.process { commit(:post, 'payments/completePayment', post) } - end - else - commit(:post, 'payments', post) - end + add_auth_purchase(post, money, payment, options) + post[:capture] = true unless payment.is_a?(Check) + + commit(:post, 'payments', post) end def authorize(money, payment, options = {}) post = {} - add_invoice(post, money, options) - add_payment(post, payment, options) - add_address(post, payment, options) - add_metadata(post, options) - add_ewallet(post, options) - post[:capture] = false + add_auth_purchase(post, money, payment, options) + post[:capture] = false unless payment.is_a?(Check) + commit(:post, 'payments', post) end def capture(money, authorization, options = {}) post = {} - commit(:post, "payments/#{authorization}/capture", post) + commit(:post, "payments/#{add_reference(authorization)}/capture", post) end def refund(money, authorization, options = {}) post = {} - post[:payment] = authorization + post[:payment] = add_reference(authorization) add_invoice(post, money, options) add_metadata(post, options) + add_ewallet(post, options) + commit(:post, 'refunds', post) end def void(authorization, options = {}) post = {} - commit(:delete, "payments/#{authorization}", post) + commit(:delete, "payments/#{add_reference(authorization)}", post) end - # Gateway returns an error if trying to run a $0 auth as invalid payment amount - # Gateway does not support void on a card transaction and refunds can only be done on completed transactions - # (such as a purchase). Authorize transactions are considered 'active' and not 'complete' until they are captured. def verify(credit_card, options = {}) - MultiResponse.run do |r| - r.process { purchase(100, credit_card, options) } - r.process { refund(100, r.authorization, options) } - end + authorize(0, credit_card, options) + end + + def store(payment, options = {}) + post = {} + add_payment(post, payment, options) + add_customer_data(post, payment, options, 'store') + add_metadata(post, options) + add_ewallet(post, options) + add_payment_fields(post, options) + add_payment_urls(post, options, 'store') + add_address(post, payment, options) + commit(:post, 'customers', post) + end + + def unstore(customer) + commit(:delete, "customers/#{add_reference(customer)}", {}) end def supports_scrubbing? @@ -87,25 +84,34 @@ def scrub(transcript) transcript. gsub(%r((Access_key: )\w+), '\1[FILTERED]'). gsub(%r(("number\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("account_number\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("routing_number\\?":\\?")\d+), '\1[FILTERED]'). gsub(%r(("cvv\\?":\\?")\d+), '\1[FILTERED]') end private - def payment_is_ach?(options) - return unless options[:pm_type] + def add_reference(authorization) + return unless authorization - return true if options[:pm_type].include?('_bank') + authorization.split('|')[0] end - def payment_is_card?(options) - return unless options[:pm_type] - - return true if options[:pm_type].include?('_card') + def add_auth_purchase(post, money, payment, options) + add_invoice(post, money, options) + add_payment(post, payment, options) + add_customer_data(post, payment, options) + add_3ds(post, payment, options) + add_address(post, payment, options) + add_metadata(post, options) + add_recurrence_type(post, options) + add_ewallet(post, options) + add_payment_fields(post, options) + add_payment_urls(post, options) end def add_address(post, creditcard, options) - return unless address = options[:address] + return unless address = options[:billing_address] post[:address] = {} # name and line_1 are required at the gateway @@ -116,22 +122,48 @@ def add_address(post, creditcard, options) post[:address][:state] = address[:state] if address[:state] post[:address][:country] = address[:country] if address[:country] post[:address][:zip] = address[:zip] if address[:zip] - post[:address][:phone_number] = address[:phone] if address[:phone] + post[:address][:phone_number] = address[:phone].gsub(/\D/, '') if address[:phone] end def add_invoice(post, money, options) - post[:amount] = amount(money).to_f.to_s + post[:amount] = money.zero? ? 0 : amount(money).to_f.to_s post[:currency] = (options[:currency] || currency(money)) + post[:merchant_reference_id] = options[:merchant_reference_id] || options[:order_id] end def add_payment(post, payment, options) - if payment_is_card?(options) + if payment.is_a?(CreditCard) add_creditcard(post, payment, options) - elsif payment_is_ach?(options) + elsif payment.is_a?(Check) add_ach(post, payment, options) + else + add_tokens(post, payment, options) end end + def add_stored_credential(post, options) + add_network_reference_id(post, options) + add_initiation_type(post, options) + end + + def add_network_reference_id(post, options) + return unless (options[:stored_credential] && options[:stored_credential][:reason_type] == 'recurring') || options[:network_transaction_id] + + network_transaction_id = options[:network_transaction_id] || options[:stored_credential][:network_transaction_id] + post[:payment_method][:fields][:network_reference_id] = network_transaction_id unless network_transaction_id&.empty? + end + + def add_initiation_type(post, options) + return unless options[:stored_credential] || options[:initiation_type] + + initiation_type = options[:initiation_type] || options[:stored_credential][:reason_type] + post[:initiation_type] = initiation_type if initiation_type + end + + def add_recurrence_type(post, options) + post[:recurrence_type] = options[:recurrence_type] if options[:recurrence_type] + end + def add_creditcard(post, payment, options) post[:payment_method] = {} post[:payment_method][:fields] = {} @@ -141,8 +173,18 @@ def add_creditcard(post, payment, options) pm_fields[:number] = payment.number pm_fields[:expiration_month] = payment.month.to_s pm_fields[:expiration_year] = payment.year.to_s - pm_fields[:cvv] = payment.verification_value.to_s pm_fields[:name] = "#{payment.first_name} #{payment.last_name}" + pm_fields[:cvv] = payment.verification_value.to_s unless valid_network_transaction_id?(options) || payment.verification_value.blank? + add_stored_credential(post, options) + end + + def send_customer_object?(options) + options[:stored_credential] && options[:stored_credential][:reason_type] == 'recurring' + end + + def valid_network_transaction_id?(options) + network_transaction_id = options[:network_tansaction_id] || options.dig(:stored_credential_options, :network_transaction_id) || options.dig(:stored_credential, :network_transaction_id) + return network_transaction_id.present? end def add_ach(post, payment, options) @@ -158,12 +200,96 @@ def add_ach(post, payment, options) post[:payment_method][:fields][:payment_purpose] = options[:payment_purpose] if options[:payment_purpose] end + def add_tokens(post, payment, options) + return unless payment.respond_to?(:split) + + customer_id, card_id = payment.split('|') + + post[:customer] = customer_id unless send_customer_object?(options) + post[:payment_method] = card_id + end + + def add_3ds(post, payment, options) + if options[:execute_threed] == true + post[:payment_method_options] = { '3d_required' => true } + elsif three_d_secure = options[:three_d_secure] + post[:payment_method_options] = {} + post[:payment_method_options]['3d_required'] = three_d_secure[:required] + post[:payment_method_options]['3d_version'] = three_d_secure[:version] + post[:payment_method_options][:cavv] = three_d_secure[:cavv] + post[:payment_method_options][:eci] = three_d_secure[:eci] + post[:payment_method_options][:xid] = three_d_secure[:xid] + post[:payment_method_options][:ds_trans_id] = three_d_secure[:ds_transaction_id] + end + end + def add_metadata(post, options) post[:metadata] = options[:metadata] if options[:metadata] end def add_ewallet(post, options) - post[:ewallet_id] = options[:ewallet_id] if options[:ewallet_id] + post[:ewallet] = options[:ewallet_id] if options[:ewallet_id] + end + + def add_payment_fields(post, options) + post[:description] = options[:description] if options[:description] + post[:statement_descriptor] = options[:statement_descriptor] if options[:statement_descriptor] + end + + def add_payment_urls(post, options, action = '') + if action == 'store' + url_location = post[:payment_method] + else + url_location = post + end + + url_location[:complete_payment_url] = options[:complete_payment_url] if options[:complete_payment_url] + url_location[:error_payment_url] = options[:error_payment_url] if options[:error_payment_url] + end + + def add_customer_data(post, payment, options, action = '') + phone_number = options.dig(:billing_address, :phone) || options.dig(:billing_address, :phone_number) + post[:phone_number] = phone_number.gsub(/\D/, '') unless phone_number.nil? + post[:email] = options[:email] unless send_customer_object?(options) + return if payment.is_a?(String) + return add_customer_id(post, options) if options[:customer_id] + + if action == 'store' + post.merge!(customer_fields(payment, options)) + else + post[:customer] = customer_fields(payment, options) unless send_customer_object?(options) + end + end + + def customer_fields(payment, options) + return if options[:customer_id] + + customer_address = address(options) + customer_data = {} + customer_data[:name] = "#{payment.first_name} #{payment.last_name}" unless payment.is_a?(String) + customer_data[:addresses] = [customer_address] if customer_address + customer_data + end + + def address(options) + return unless address = options[:billing_address] + + formatted_address = {} + + formatted_address[:name] = address[:name] if address[:name] + formatted_address[:line_1] = address[:address1] if address[:address1] + formatted_address[:line_2] = address[:address2] if address[:address2] + formatted_address[:city] = address[:city] if address[:city] + formatted_address[:state] = address[:state] if address[:state] + formatted_address[:country] = address[:country] if address[:country] + formatted_address[:zip] = address[:zip] if address[:zip] + formatted_address[:phone_number] = address[:phone].gsub(/\D/, '') if address[:phone] + + formatted_address + end + + def add_customer_id(post, options) + post[:customer] = options[:customer_id] if options[:customer_id] end def parse(body) @@ -189,8 +315,19 @@ def commit(method, action, parameters) ) end + # We need to revert the work of ActiveSupport JSON encoder to prevent discrepancies + # Between the signature and the actual request body + def revert_json_html_encoding!(string) + { + '\\u003e' => '>', + '\\u003c' => '<', + '\\u0026' => '&' + }.each { |k, v| string.gsub! k, v } + end + def api_request(method, url, rel_path, params) params == {} ? body = '' : body = params.to_json + revert_json_html_encoding!(body) if defined?(ActiveSupport::JSON::Encoding) && ActiveSupport::JSON::Encoding.escape_html_entities_in_json parse(ssl_request(method, url, body, headers(rel_path, body))) end @@ -238,7 +375,9 @@ def message_from(response) end def authorization_from(response) - response.dig('data') ? response.dig('data', 'id') : response.dig('status', 'operation_id') + id = response.dig('data') ? response.dig('data', 'id') : response.dig('status', 'operation_id') + + "#{id}|#{response.dig('data', 'default_payment_method')}" end def error_code_from(response) diff --git a/lib/active_merchant/billing/gateways/reach.rb b/lib/active_merchant/billing/gateways/reach.rb new file mode 100644 index 00000000000..41c2c6c9926 --- /dev/null +++ b/lib/active_merchant/billing/gateways/reach.rb @@ -0,0 +1,284 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class ReachGateway < Gateway + self.test_url = 'https://checkout.rch.how/' + self.live_url = 'https://checkout.rch.io/' + + self.supported_countries = %w(AE AG AL AM AT AU AW AZ BA BB BD BE BF BG BH BJ BM BN BO BR BS BW BZ CA CD CF + CH CI CL CM CN CO CR CU CV CY CZ DE DJ DK DM DO DZ EE EG ES ET FI FJ FK FR GA + GB GD GE GG GH GI GN GR GT GU GW GY HK HN HR HU ID IE IL IM IN IS IT JE JM JO + JP KE KG KH KM KN KR KW KY KZ LA LC LK LR LT LU LV LY MA MD MK ML MN MO MR MS + MT MU MV MW MX MY MZ NA NC NE NG NI NL NO NP NZ OM PA PE PF PG PH PK PL PT PY + QA RO RS RW SA SB SC SE SG SH SI SK SL SN SO SR ST SV SY SZ TD TG TH TN TO TR + TT TV TW TZ UG US UY UZ VC VN VU WF WS YE ZM) + + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa diners_club american_express jcb master discover maestro] + + self.homepage_url = 'https://www.withreach.com/' + self.display_name = 'Reach' + self.currencies_without_fractions = %w(BIF BYR CLF CLP CVE DJF GNF ISK JPY KMF KRW PYG RWF UGX UYI VND VUV XAF XOF XPF IDR MGA MRO) + + API_VERSION = 'v2.22'.freeze + STANDARD_ERROR_CODE_MAPPING = {} + PAYMENT_METHOD_MAP = { + american_express: 'AMEX', + cabal: 'CABAL', + check: 'ACH', + dankort: 'DANKORT', + diners_club: 'DINERS', + discover: 'DISC', + elo: 'ELO', + jcb: 'JCB', + maestro: 'MAESTRO', + master: 'MC', + naranja: 'NARANJA', + union_pay: 'UNIONPAY', + visa: 'VISA' + } + + def initialize(options = {}) + requires!(options, :merchant_id, :secret) + super + end + + def authorize(money, payment, options = {}) + request = build_checkout_request(money, payment, options) + add_custom_fields_data(request, options) + add_customer_data(request, options, payment) + add_stored_credentials(request, options) + post = { request: request, card: add_payment(payment, options) } + if options[:stored_credential] + MultiResponse.run(:use_first_response) do |r| + r.process { commit('checkout', post) } + r.process do + r2 = get_network_payment_reference(r.responses[0]) + r.params[:network_transaction_id] = r2.message + r2 + end + end + else + commit('checkout', post) + end + end + + def purchase(money, payment, options = {}) + options[:capture] = true + authorize(money, payment, options) + end + + def capture(money, authorization, options = {}) + post = { request: { MerchantId: @options[:merchant_id], OrderId: authorization } } + commit('capture', post) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r(((MerchantId)[% \w]+[%]\d{2})[\w -]+), '\1[FILTERED]'). + gsub(%r((signature=)[\w%]+), '\1[FILTERED]\2'). + gsub(%r((Number%22%3A%22)[\d]+), '\1[FILTERED]\2'). + gsub(%r((VerificationCode%22%3A)[\d]+), '\1[FILTERED]\2') + end + + def refund(amount, authorization, options = {}) + currency = options[:currency] || currency(options[:amount]) + post = { + request: { + MerchantId: @options[:merchant_id], + OrderId: authorization, + ReferenceId: options[:order_id] || options[:reference_id], + Amount: localized_amount(amount, currency) + } + } + commit('refund', post) + end + + def void(authorization, options = {}) + post = { + request: { + MerchantId: @options[:merchant_id], + OrderId: authorization + } + } + + commit('cancel', post) + end + + def verify(credit_card, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + + private + + def build_checkout_request(amount, payment, options) + currency = options[:currency] || currency(options[:amount]) + { + MerchantId: @options[:merchant_id], + ReferenceId: options[:order_id], + ConsumerCurrency: currency, + Capture: options[:capture] || false, + PaymentMethod: PAYMENT_METHOD_MAP.fetch(payment.brand.to_sym, 'unsupported'), + Items: [ + Sku: options[:item_sku] || SecureRandom.alphanumeric, + ConsumerPrice: localized_amount(amount, currency), + Quantity: (options[:item_quantity] || 1) + ] + } + end + + def add_payment(payment, options) + ntid = options.dig(:stored_credential, :network_transaction_id) + cvv_or_previos_reference = (ntid ? { PreviousNetworkPaymentReference: ntid } : { VerificationCode: payment.verification_value }) + { + Name: payment.name, + Number: payment.number, + Expiry: { Month: payment.month, Year: payment.year } + }.merge!(cvv_or_previos_reference) + end + + def add_customer_data(request, options, payment) + address = options[:billing_address] || options[:address] + + return if address.blank? + + request[:Consumer] = { + Name: payment.respond_to?(:name) ? payment.name : address[:name], + Email: options[:email], + Address: address[:address1], + City: address[:city], + Country: address[:country] + }.compact + end + + def add_stored_credentials(request, options) + request[:PaymentModel] = payment_model(options) || '' + request[:DeviceFingerprint] = options[:device_fingerprint] if options[:device_fingerprint] + end + + def payment_model(options) + stored_credential = options[:stored_credential] + return options[:payment_model] if options[:payment_model] + return 'CIT-One-Time' unless stored_credential + + payment_model_options = { + initial_transaction: { + 'cardholder' => { + 'installment' => 'CIT-Setup-Scheduled', + 'unscheduled' => 'CIT-Setup-Unscheduled-MIT', + 'recurring' => 'CIT-Setup-Unscheduled' + } + }, + no_initial_transaction: { + 'cardholder' => { + 'unscheduled' => 'CIT-Subsequent-Unscheduled' + }, + 'merchant' => { + 'recurring' => 'MIT-Subsequent-Scheduled', + 'unscheduled' => 'MIT-Subsequent-Unscheduled' + } + } + } + initial = stored_credential[:initial_transaction] ? :initial_transaction : :no_initial_transaction + payment_model_options[initial].dig(stored_credential[:initiator], stored_credential[:reason_type]) + end + + def add_custom_fields_data(request, options) + add_shipping_data(request, options) if options[:taxes].present? + request[:RateOfferId] = options[:rate_offer_id] if options[:rate_offer_id].present? + request[:Items] = options[:items] if options[:items].present? + end + + def add_shipping_data(request, options) + request[:Shipping] = { + ConsumerPrice: options[:price], + ConsumerTaxes: options[:taxes], + ConsumerDuty: options[:duty] + } + request[:Consignee] = { + Name: options[:consignee_name], + Address: options[:consignee_address], + City: options[:consignee_city], + Country: options[:consignee_country] + } + end + + def sign_body(body) + Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', @options[:secret].encode('utf-8'), body.encode('utf-8'))) + end + + def parse(body) + hash_response = URI.decode_www_form(body).to_h + hash_response['response'] = JSON.parse(hash_response['response']) + + hash_response + end + + def format_and_sign(post) + post[:request] = post[:request].to_json + post[:card] = post[:card].to_json if post[:card].present? + post[:signature] = sign_body(post[:request]) + post + end + + def get_network_payment_reference(response) + parameters = { request: { MerchantId: @options[:merchant_id], OrderId: response.params['response']['OrderId'] } } + body = post_data format_and_sign(parameters) + + raw_response = ssl_request :post, url('query'), body, {} + response = parse(raw_response) + message = response.dig('response', 'Payment', 'NetworkPaymentReference') + Response.new(true, message, {}) + end + + def commit(action, parameters) + body = post_data format_and_sign(parameters) + raw_response = ssl_post url(action), body + response = parse(raw_response) + + Response.new( + success_from(response), + message_from(response) || '', + response, + authorization: authorization_from(response['response']), + # avs_result: AVSResult.new(code: response['some_avs_response_key']), + # cvv_result: CVVResult.new(response['some_cvv_response_key']), + test: test?, + error_code: error_code_from(response) + ) + rescue ActiveMerchant::ResponseError => e + Response.new(false, (e.response.body.present? ? e.response.body : e.response.msg), {}, test: test?) + end + + def success_from(response) + response.dig('response', 'Error').blank? + end + + def message_from(response) + success_from(response) ? '' : response.dig('response', 'Error', 'ReasonCode') + end + + def authorization_from(response) + response['OrderId'] + end + + def post_data(params) + params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') + end + + def error_code_from(response) + response['response']['Error']['Code'] unless success_from(response) + end + + def url(action) + "#{test? ? test_url : live_url}#{API_VERSION}/#{action}" + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/redsys.rb b/lib/active_merchant/billing/gateways/redsys.rb index a1819518d3c..260c4d7d83f 100644 --- a/lib/active_merchant/billing/gateways/redsys.rb +++ b/lib/active_merchant/billing/gateways/redsys.rb @@ -39,7 +39,7 @@ class RedsysGateway < Gateway self.live_url = 'https://sis.redsys.es/sis/operaciones' self.test_url = 'https://sis-t.redsys.es:25443/sis/operaciones' - self.supported_countries = ['ES'] + self.supported_countries = %w[ES FR GB IT PL PT] self.default_currency = 'EUR' self.money_format = :cents @@ -91,7 +91,7 @@ class RedsysGateway < Gateway # More operations are supported by the gateway itself, but # are not supported in this library. SUPPORTED_TRANSACTIONS = { - purchase: 'A', + purchase: '0', authorize: '1', capture: '2', refund: '3', @@ -266,9 +266,13 @@ def refund(money, authorization, options = {}) end def verify(creditcard, options = {}) - MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, creditcard, options) } - r.process(:ignore_result) { void(r.authorization, options) } + if options[:sca_exemption_behavior_override] == 'endpoint_and_ntid' + purchase(0, creditcard, options) + else + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, creditcard, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end end end @@ -534,6 +538,7 @@ def build_merchant_data(xml, data, options = {}) xml.DS_MERCHANT_COF_INI data[:DS_MERCHANT_COF_INI] xml.DS_MERCHANT_COF_TYPE data[:DS_MERCHANT_COF_TYPE] xml.DS_MERCHANT_COF_TXNID data[:DS_MERCHANT_COF_TXNID] if data[:DS_MERCHANT_COF_TXNID] + xml.DS_MERCHANT_DIRECTPAYMENT 'false' if options[:stored_credential][:initial_transaction] end end end @@ -685,7 +690,7 @@ def encrypt(key, order_id) cipher = OpenSSL::Cipher.new('DES3') cipher.encrypt - cipher.key = Base64.strict_decode64(key) + cipher.key = Base64.urlsafe_decode64(key) # The OpenSSL default of an all-zeroes ("\\0") IV is used. cipher.padding = 0 diff --git a/lib/active_merchant/billing/gateways/safe_charge.rb b/lib/active_merchant/billing/gateways/safe_charge.rb index 49554a31454..42ed13f3ead 100644 --- a/lib/active_merchant/billing/gateways/safe_charge.rb +++ b/lib/active_merchant/billing/gateways/safe_charge.rb @@ -73,10 +73,10 @@ def refund(money, authorization, options = {}) add_transaction_data('Credit', post, money, options.merge!({ currency: original_currency })) post[:sg_CreditType] = 2 post[:sg_AuthCode] = auth - post[:sg_TransactionID] = transaction_id post[:sg_CCToken] = token post[:sg_ExpMonth] = exp_month post[:sg_ExpYear] = exp_year + post[:sg_TransactionID] = transaction_id unless options[:unreferenced_refund] commit(post) end @@ -86,6 +86,7 @@ def credit(money, payment, options = {}) add_payment(post, payment, options) add_transaction_data('Credit', post, money, options) + add_customer_details(post, payment, options) post[:sg_CreditType] = 1 diff --git a/lib/active_merchant/billing/gateways/sage.rb b/lib/active_merchant/billing/gateways/sage.rb index b0c56ee02e8..25817aa99f9 100644 --- a/lib/active_merchant/billing/gateways/sage.rb +++ b/lib/active_merchant/billing/gateways/sage.rb @@ -260,11 +260,15 @@ def commit(action, params, source) url = url(params, source) response = parse(ssl_post(url, post_data(action, params)), source) - Response.new(success?(response), response[:message], response, + Response.new( + success?(response), + response[:message], + response, test: test?, authorization: authorization_from(response, source), avs_result: { code: response[:avs_result] }, - cvv_result: response[:cvv_result]) + cvv_result: response[:cvv_result] + ) end def url(params, source) @@ -380,8 +384,12 @@ def commit(action, request) message = success ? 'Succeeded' : 'Failed' end - Response.new(success, message, response, - authorization: response[:guid]) + Response.new( + success, + message, + response, + authorization: response[:guid] + ) end ENVELOPE_NAMESPACES = { diff --git a/lib/active_merchant/billing/gateways/sage_pay.rb b/lib/active_merchant/billing/gateways/sage_pay.rb index ce28ac566ec..0f062f8caab 100644 --- a/lib/active_merchant/billing/gateways/sage_pay.rb +++ b/lib/active_merchant/billing/gateways/sage_pay.rb @@ -346,14 +346,18 @@ def format_date(month, year) def commit(action, parameters) response = parse(ssl_post(url_for(action), post_data(action, parameters))) - Response.new(response['Status'] == APPROVED, message_from(response), response, + Response.new( + response['Status'] == APPROVED, + message_from(response), + response, test: test?, authorization: authorization_from(response, parameters, action), avs_result: { street_match: AVS_CODE[response['AddressResult']], postal_match: AVS_CODE[response['PostCodeResult']] }, - cvv_result: CVV_CODE[response['CV2Result']]) + cvv_result: CVV_CODE[response['CV2Result']] + ) end def authorization_from(response, params, action) @@ -427,7 +431,7 @@ def add_pair(post, key, value, options = {}) def past_purchase_reference?(payment_method) return false unless payment_method.is_a?(String) - payment_method.split(';').last == 'purchase' + %w(purchase repeat).include?(payment_method.split(';').last) end end end diff --git a/lib/active_merchant/billing/gateways/sallie_mae.rb b/lib/active_merchant/billing/gateways/sallie_mae.rb index af13651eae0..fa7f1f9f8c3 100644 --- a/lib/active_merchant/billing/gateways/sallie_mae.rb +++ b/lib/active_merchant/billing/gateways/sallie_mae.rb @@ -120,9 +120,13 @@ def commit(action, money, parameters) end response = parse(ssl_post(self.live_url, parameters.to_post_data) || '') - Response.new(successful?(response), message_from(response), response, + Response.new( + successful?(response), + message_from(response), + response, test: test?, - authorization: response['refcode']) + authorization: response['refcode'] + ) end def successful?(response) diff --git a/lib/active_merchant/billing/gateways/secure_net.rb b/lib/active_merchant/billing/gateways/secure_net.rb index 474babbd600..a82a8bc2ec4 100644 --- a/lib/active_merchant/billing/gateways/secure_net.rb +++ b/lib/active_merchant/billing/gateways/secure_net.rb @@ -79,11 +79,15 @@ def commit(request) data = ssl_post(url, xml, 'Content-Type' => 'text/xml') response = parse(data) - Response.new(success?(response), message_from(response), response, + Response.new( + success?(response), + message_from(response), + response, test: test?, authorization: build_authorization(response), avs_result: { code: response[:avs_result_code] }, - cvv_result: response[:card_code_response_code]) + cvv_result: response[:card_code_response_code] + ) end def build_request(request) diff --git a/lib/active_merchant/billing/gateways/secure_pay.rb b/lib/active_merchant/billing/gateways/secure_pay.rb index 2ad96517097..faddf42c301 100644 --- a/lib/active_merchant/billing/gateways/secure_pay.rb +++ b/lib/active_merchant/billing/gateways/secure_pay.rb @@ -56,12 +56,16 @@ def commit(action, money, parameters) message = message_from(response) - Response.new(success?(response), message, response, + Response.new( + success?(response), + message, + response, test: test?, authorization: response[:transaction_id], fraud_review: fraud_review?(response), avs_result: { code: response[:avs_result_code] }, - cvv_result: response[:card_code]) + cvv_result: response[:card_code] + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/secure_pay_au.rb b/lib/active_merchant/billing/gateways/secure_pay_au.rb index fc993767a80..13f37c96254 100644 --- a/lib/active_merchant/billing/gateways/secure_pay_au.rb +++ b/lib/active_merchant/billing/gateways/secure_pay_au.rb @@ -183,9 +183,13 @@ def build_request(action, body) def commit(action, request) response = parse(ssl_post(test? ? self.test_url : self.live_url, build_request(action, request))) - Response.new(success?(response), message_from(response), response, + Response.new( + success?(response), + message_from(response), + response, test: test?, - authorization: authorization_from(response)) + authorization: authorization_from(response) + ) end def build_periodic_item(action, money, credit_card, options) @@ -239,9 +243,13 @@ def commit_periodic(request) my_request = build_periodic_request(request) response = parse(ssl_post(test? ? self.test_periodic_url : self.live_periodic_url, my_request)) - Response.new(success?(response), message_from(response), response, + Response.new( + success?(response), + message_from(response), + response, test: test?, - authorization: authorization_from(response)) + authorization: authorization_from(response) + ) end def success?(response) diff --git a/lib/active_merchant/billing/gateways/secure_pay_tech.rb b/lib/active_merchant/billing/gateways/secure_pay_tech.rb index a866bddb17b..80f26087b9c 100644 --- a/lib/active_merchant/billing/gateways/secure_pay_tech.rb +++ b/lib/active_merchant/billing/gateways/secure_pay_tech.rb @@ -84,9 +84,13 @@ def parse(body) def commit(action, post) response = parse(ssl_post(self.live_url, post_data(action, post))) - Response.new(response[:result_code] == 1, message_from(response), response, + Response.new( + response[:result_code] == 1, + message_from(response), + response, test: test?, - authorization: response[:merchant_transaction_reference]) + authorization: response[:merchant_transaction_reference] + ) end def message_from(result) diff --git a/lib/active_merchant/billing/gateways/securion_pay.rb b/lib/active_merchant/billing/gateways/securion_pay.rb index 8b63029b6c0..0489ec924dd 100644 --- a/lib/active_merchant/billing/gateways/securion_pay.rb +++ b/lib/active_merchant/billing/gateways/securion_pay.rb @@ -223,14 +223,28 @@ def commit(url, parameters = nil, options = {}, method = nil) end response = api_request(url, parameters, options, method) - success = !response.key?('error') + success = success?(response) - Response.new(success, + Response.new( + success, (success ? 'Transaction approved' : response['error']['message']), response, test: test?, - authorization: (success ? response['id'] : response['error']['charge']), - error_code: (success ? nil : STANDARD_ERROR_CODE_MAPPING[response['error']['code']])) + authorization: authorization_from(url, response), + error_code: (success ? nil : STANDARD_ERROR_CODE_MAPPING[response['error']['code']]) + ) + end + + def authorization_from(action, response) + if action == 'customers' && success?(response) && response['cards'].present? + response['cards'].first['id'] + else + success?(response) ? response['id'] : response['error']['charge'] + end + end + + def success?(response) + !response.key?('error') end def headers(options = {}) @@ -287,8 +301,8 @@ def api_request(endpoint, parameters = nil, options = {}, method = nil) response end - def json_error(raw_response) - msg = 'Invalid response received from the SecurionPay API.' + def json_error(raw_response, gateway_name = 'SecurionPay') + msg = "Invalid response received from the #{gateway_name} API." msg += " (The raw response returned by the API was #{raw_response.inspect})" { 'error' => { diff --git a/lib/active_merchant/billing/gateways/shift4.rb b/lib/active_merchant/billing/gateways/shift4.rb new file mode 100644 index 00000000000..ca6feaa5eaf --- /dev/null +++ b/lib/active_merchant/billing/gateways/shift4.rb @@ -0,0 +1,345 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class Shift4Gateway < Gateway + self.test_url = 'https://utgapi.shift4test.com/api/rest/v1/' + self.live_url = 'https://utg.shift4api.net/api/rest/v1/' + + self.supported_countries = %w(US CA CU HT DO PR JM TT GP MQ BS BB LC CW AW VC VI GD AG DM KY KN SX TC MF VG BQ AI BL MS) + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover] + + self.homepage_url = 'https://shift4.com' + self.display_name = 'Shift4' + + RECURRING_TYPE_TRANSACTIONS = %w(recurring installment) + TRANSACTIONS_WITHOUT_RESPONSE_CODE = %w(accesstoken add) + SUCCESS_TRANSACTION_STATUS = %w(A) + DISALLOWED_ENTRY_MODE_ACTIONS = %w(capture refund add verify) + URL_POSTFIX_MAPPING = { + 'accesstoken' => 'credentials', + 'add' => 'tokens', + 'verify' => 'cards' + } + STANDARD_ERROR_CODE_MAPPING = { + 'incorrect_number' => STANDARD_ERROR_CODE[:incorrect_number], + 'invalid_number' => STANDARD_ERROR_CODE[:invalid_number], + 'invalid_expiry_month' => STANDARD_ERROR_CODE[:invalid_expiry_date], + 'invalid_expiry_year' => STANDARD_ERROR_CODE[:invalid_expiry_date], + 'invalid_cvc' => STANDARD_ERROR_CODE[:invalid_cvc], + 'expired_card' => STANDARD_ERROR_CODE[:expired_card], + 'insufficient_funds' => STANDARD_ERROR_CODE[:card_declined], + 'incorrect_cvc' => STANDARD_ERROR_CODE[:incorrect_cvc], + 'incorrect_zip' => STANDARD_ERROR_CODE[:incorrect_zip], + 'card_declined' => STANDARD_ERROR_CODE[:card_declined], + 'processing_error' => STANDARD_ERROR_CODE[:processing_error], + 'lost_or_stolen' => STANDARD_ERROR_CODE[:card_declined], + 'suspected_fraud' => STANDARD_ERROR_CODE[:card_declined], + 'expired_token' => STANDARD_ERROR_CODE[:card_declined] + } + + def initialize(options = {}) + requires!(options, :client_guid, :auth_token) + @client_guid = options[:client_guid] + @auth_token = options[:auth_token] + @access_token = options[:access_token] + super + end + + def purchase(money, payment_method, options = {}) + post = {} + action = 'sale' + + payment_method = get_card_token(payment_method) if payment_method.is_a?(String) + add_datetime(post, options) + add_invoice(post, money, options) + add_clerk(post, options) + add_transaction(post, options) + add_card(action, post, payment_method, options) + add_card_present(post, options) + add_customer(post, payment_method, options) + + commit(action, post, options) + end + + def authorize(money, payment_method, options = {}) + post = {} + action = 'authorization' + + payment_method = get_card_token(payment_method) if payment_method.is_a?(String) + add_datetime(post, options) + add_invoice(post, money, options) + add_clerk(post, options) + add_transaction(post, options) + add_card(action, post, payment_method, options) + add_card_present(post, options) + add_customer(post, payment_method, options) + + commit(action, post, options) + end + + def capture(money, authorization, options = {}) + post = {} + action = 'capture' + options[:invoice] = get_invoice(authorization) + + add_datetime(post, options) + add_invoice(post, money, options) + add_clerk(post, options) + add_transaction(post, options) + add_card(action, post, get_card_token(authorization), options) + + commit(action, post, options) + end + + def refund(money, payment_method, options = {}) + post = {} + action = 'refund' + + add_datetime(post, options) + add_invoice(post, money, options) + add_clerk(post, options) + add_transaction(post, options) + card_token = payment_method.is_a?(CreditCard) ? get_card_token(payment_method) : payment_method + add_card(action, post, card_token, options) + add_card_present(post, options) + + commit(action, post, options) + end + + alias credit refund + + def void(authorization, options = {}) + options[:invoice] = get_invoice(authorization) + commit('invoice', {}, options) + end + + def verify(credit_card, options = {}) + post = {} + action = 'verify' + post[:transaction] = {} + + add_datetime(post, options) + add_card(action, post, credit_card, options) + add_customer(post, credit_card, options) + add_card_on_file(post[:transaction], options) + + commit(action, post, options) + end + + def store(credit_card, options = {}) + post = {} + action = 'add' + + add_datetime(post, options) + add_card(action, post, credit_card, options) + add_customer(post, credit_card, options) + + commit(action, post, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r(("Number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("expirationDate\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("FirstName\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("LastName\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("securityCode\\?":{\\?"[\w]+\\?":[\d]+,\\?"value\\?":\\?")[\d]*)i, '\1[FILTERED]') + end + + def setup_access_token + post = {} + add_credentials(post, options) + add_datetime(post, options) + + response = commit('accesstoken', post, request_headers('accesstoken', options)) + raise OAuthResponseError.new(response, response.params.fetch('result', [{}]).first.dig('error', 'longText')) unless response.success? + + response.params['result'].first['credential']['accessToken'] + end + + private + + def add_credentials(post, options) + post[:credential] = {} + post[:credential][:clientGuid] = @client_guid + post[:credential][:authToken] = @auth_token + end + + def add_clerk(post, options) + post[:clerk] = {} + post[:clerk][:numericId] = options[:clerk_id] || '1' + end + + def add_invoice(post, money, options) + post[:amount] = {} + post[:amount][:total] = amount(money.to_f) + post[:amount][:tax] = options[:tax].to_f || 0.0 + end + + def add_datetime(post, options) + post[:dateTime] = options[:date_time] || current_date_time(options) + end + + def add_transaction(post, options) + post[:transaction] = {} + post[:transaction][:invoice] = options[:invoice] || Time.new.to_i.to_s[1..3] + rand.to_s[2..7] + post[:transaction][:notes] = options[:notes] if options[:notes].present? + post[:transaction][:vendorReference] = options[:order_id] + + add_purchase_card(post[:transaction], options) + add_card_on_file(post[:transaction], options) + end + + def add_card(action, post, payment_method, options) + post[:card] = {} + post[:card][:entryMode] = options[:entry_mode] || 'M' unless DISALLOWED_ENTRY_MODE_ACTIONS.include?(action) + if payment_method.is_a?(CreditCard) + post[:card][:expirationDate] = "#{format(payment_method.month, :two_digits)}#{format(payment_method.year, :two_digits)}" + post[:card][:number] = payment_method.number + post[:card][:securityCode] = {} + post[:card][:securityCode][:indicator] = 1 + post[:card][:securityCode][:value] = payment_method.verification_value + else + post[:card] = {} if post[:card].nil? + post[:card][:token] = {} + post[:card][:token][:value] = payment_method + post[:card][:expirationDate] = options[:expiration_date] if options[:expiration_date] + end + end + + def add_card_present(post, options) + post[:card] = {} unless post[:card].present? + + post[:card][:present] = options[:card_present] || 'N' + end + + def add_customer(post, card, options) + address = options[:billing_address] || {} + + post[:customer] = {} + post[:customer][:addressLine1] = address[:address1] if address[:address1] + post[:customer][:postalCode] = address[:zip] if address[:zip] && !address[:zip]&.to_s&.empty? + post[:customer][:firstName] = card.first_name if card.is_a?(CreditCard) && card.first_name + post[:customer][:lastName] = card.last_name if card.is_a?(CreditCard) && card.last_name + post[:customer][:emailAddress] = options[:email] if options[:email] + post[:customer][:ipAddress] = options[:ip] if options[:ip] + end + + def add_purchase_card(post, options) + return unless options[:customer_reference] || options[:destination_postal_code] || options[:product_descriptors] + + post[:purchaseCard] = {} + post[:purchaseCard][:customerReference] = options[:customer_reference] if options[:customer_reference] + post[:purchaseCard][:destinationPostalCode] = options[:destination_postal_code] if options[:destination_postal_code] + post[:purchaseCard][:productDescriptors] = options[:product_descriptors] if options[:product_descriptors] + end + + def add_card_on_file(post, options) + return unless options[:stored_credential] || options[:usage_indicator] || options[:indicator] || options[:scheduled_indicator] || options[:transaction_id] + + stored_credential = options[:stored_credential] || {} + post[:cardOnFile] = {} + post[:cardOnFile][:usageIndicator] = options[:usage_indicator] || (stored_credential[:initial_transaction] ? '01' : '02') + post[:cardOnFile][:indicator] = options[:indicator] || '01' + post[:cardOnFile][:scheduledIndicator] = options[:scheduled_indicator] || (RECURRING_TYPE_TRANSACTIONS.include?(stored_credential[:reason_type]) ? '01' : '02') + post[:cardOnFile][:transactionId] = options[:transaction_id] || stored_credential[:network_transaction_id] if options[:transaction_id] || stored_credential[:network_transaction_id] + end + + def commit(action, parameters, option) + url_postfix = URL_POSTFIX_MAPPING[action] || 'transactions' + url = (test? ? "#{test_url}#{url_postfix}/#{action}" : "#{live_url}#{url_postfix}/#{action}") + if action == 'invoice' + response = parse(ssl_request(:delete, url, parameters.to_json, request_headers(action, option))) + else + response = parse(ssl_post(url, parameters.to_json, request_headers(action, option))) + end + + Response.new( + success_from(action, response), + message_from(action, response), + response, + authorization: authorization_from(action, response), + test: test?, + error_code: error_code_from(action, response) + ) + end + + def handle_response(response) + case response.code.to_i + when 200...300, 400, 401, 500 + response.body + else + raise ResponseError.new(response) + end + end + + def parse(body) + return {} if body == '' + + JSON.parse(body) + end + + def message_from(action, response) + success_from(action, response) ? 'Transaction successful' : (error(response)&.dig('longText') || 'Transaction declined') + end + + def error_code_from(action, response) + return unless success_from(action, response) + + STANDARD_ERROR_CODE_MAPPING[response['primaryCode']] + end + + def authorization_from(action, response) + return unless success_from(action, response) + + authorization = response.dig('result', 0, 'card', 'token', 'value').to_s + invoice = response.dig('result', 0, 'transaction', 'invoice') + authorization += "|#{invoice}" if invoice + authorization + end + + def get_card_token(authorization) + authorization.is_a?(CreditCard) ? authorization : authorization.split('|')[0] + end + + def get_invoice(authorization) + authorization.is_a?(CreditCard) ? authorization : authorization.split('|')[1] + end + + def request_headers(action, options) + headers = { + 'Content-Type' => 'application/json' + } + headers['AccessToken'] = @access_token + headers['Invoice'] = options[:invoice] if action != 'capture' && options[:invoice].present? + headers['InterfaceVersion'] = '1' + headers['InterfaceName'] = 'Spreedly' + headers['CompanyName'] = 'Spreedly' + headers + end + + def success_from(action, response) + success = error(response).nil? + success &&= SUCCESS_TRANSACTION_STATUS.include?(response['result'].first['transaction']['responseCode']) unless TRANSACTIONS_WITHOUT_RESPONSE_CODE.include?(action) + success + end + + def error(response) + server_error = { 'longText' => response['error'] } if response['error'] + server_error || response['result'].first['error'] + end + + def current_date_time(options = {}) + time_zone = options[:merchant_time_zone] || 'Pacific Time (US & Canada)' + time = Time.now.in_time_zone(time_zone) + offset = Time.now.in_time_zone(time_zone).formatted_offset + + time.strftime('%Y-%m-%dT%H:%M:%S.%3N') + offset + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/shift4_v2.rb b/lib/active_merchant/billing/gateways/shift4_v2.rb new file mode 100644 index 00000000000..f06208c2883 --- /dev/null +++ b/lib/active_merchant/billing/gateways/shift4_v2.rb @@ -0,0 +1,58 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class Shift4V2Gateway < SecurionPayGateway + # same endpont for testing + self.live_url = 'https://api.shift4.com/' + self.display_name = 'Shift4' + self.homepage_url = 'https://dev.shift4.com/us/' + + def credit(money, payment, options = {}) + post = create_post_for_auth_or_purchase(money, payment, options) + commit('credits', post, options) + end + + def create_post_for_auth_or_purchase(money, payment, options) + super.tap do |post| + add_stored_credentials(post, options) + end + end + + def add_stored_credentials(post, options) + return unless options[:stored_credential].present? + + initiator = options.dig(:stored_credential, :initiator) + reason_type = options.dig(:stored_credential, :reason_type) + + post_type = { + %w[cardholder recurring] => 'first_recurring', + %w[merchant recurring] => 'subsequent_recurring', + %w[cardholder unscheduled] => 'customer_initiated', + %w[merchant installment] => 'merchant_initiated' + }[[initiator, reason_type]] + post[:type] = post_type if post_type + end + + def headers(options = {}) + super.tap do |headers| + headers['User-Agent'] = "Shift4/v2 ActiveMerchantBindings/#{ActiveMerchant::VERSION}" + end + end + + def scrub(transcript) + super. + gsub(%r((card\[expMonth\]=)\d+), '\1[FILTERED]'). + gsub(%r((card\[expYear\]=)\d+), '\1[FILTERED]'). + gsub(%r((card\[cardholderName\]=)\w+[^ ]\w+), '\1[FILTERED]') + end + + def json_error(raw_response) + super(raw_response, 'Shift4 V2') + end + + def add_amount(post, money, options, include_currency = false) + super + post[:currency]&.upcase! + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/simetrik.rb b/lib/active_merchant/billing/gateways/simetrik.rb index 4b6af978018..5c436acab95 100644 --- a/lib/active_merchant/billing/gateways/simetrik.rb +++ b/lib/active_merchant/billing/gateways/simetrik.rb @@ -4,10 +4,13 @@ class SimetrikGateway < Gateway self.test_url = 'https://payments.sta.simetrik.com/v1' self.live_url = 'https://payments.simetrik.com/v1' - class_attribute :test_auth_url, :live_auth_url + class_attribute :test_auth_url, :live_auth_url, :test_audience, :live_audience self.test_auth_url = 'https://tenant-payments-dev.us.auth0.com/oauth/token' self.live_auth_url = 'https://tenant-payments-prod.us.auth0.com/oauth/token' + self.test_audience = 'https://tenant-payments-dev.us.auth0.com/api/v2/' + self.live_audience = 'https://tenant-payments-prod.us.auth0.com/api/v2/' + self.supported_countries = %w(PE AR) self.default_currency = 'USD' self.supported_cardtypes = %i[visa master american_express discover] @@ -39,7 +42,7 @@ class SimetrikGateway < Gateway } def initialize(options = {}) - requires!(options, :client_id, :client_secret, :audience) + requires!(options, :client_id, :client_secret) super @access_token = {} sign_access_token() @@ -69,7 +72,7 @@ def capture(money, authorization, options = {}) acquire_extra_options: options[:acquire_extra_options] || {} } } - post[:forward_payload][:amount][:vat] = options[:vat].to_f if options[:vat] + post[:forward_payload][:amount][:vat] = options[:vat].to_f / 100 if options[:vat] add_forward_route(post, options) commit('capture', post, { token_acquirer: options[:token_acquirer] }) @@ -232,8 +235,8 @@ def add_order(post, money, options) order = {} order_options = options[:order] || {} - order[:id] = order_options[:id] if order_options[:id] - order[:description] = order_options[:description] if order_options[:description] + order[:id] = options[:order_id] if options[:order_id] + order[:description] = options[:description] if options[:description] order[:installments] = order_options[:installments].to_i if order_options[:installments] order[:datetime_local_transaction] = order_options[:datetime_local_transaction] if order_options[:datetime_local_transaction] @@ -247,7 +250,7 @@ def add_amount(post, money, options) amount_obj = {} amount_obj[:total_amount] = amount(money).to_f amount_obj[:currency] = (options[:currency] || currency(money)) - amount_obj[:vat] = options[:vat].to_f if options[:vat] + amount_obj[:vat] = options[:vat].to_f / 100 if options[:vat] post[:amount] = amount_obj end @@ -277,12 +280,12 @@ def parse(body) def commit(action, parameters, url_params = {}) begin response = JSON.parse ssl_post(url(action, url_params), post_data(parameters), authorized_headers()) - rescue ResponseError => exception - case exception.response.code.to_i + rescue ResponseError => e + case e.response.code.to_i when 400...499 - response = JSON.parse exception.response.body + response = JSON.parse e.response.body else - raise exception + raise e end end @@ -351,7 +354,7 @@ def fetch_access_token login_info = {} login_info[:client_id] = @options[:client_id] login_info[:client_secret] = @options[:client_secret] - login_info[:audience] = @options[:audience] + login_info[:audience] = test? ? test_audience : live_audience login_info[:grant_type] = 'client_credentials' response = parse(ssl_post(auth_url(), login_info.to_json, { 'content-Type' => 'application/json' diff --git a/lib/active_merchant/billing/gateways/skip_jack.rb b/lib/active_merchant/billing/gateways/skip_jack.rb index 8f8851808e1..effe78d837b 100644 --- a/lib/active_merchant/billing/gateways/skip_jack.rb +++ b/lib/active_merchant/billing/gateways/skip_jack.rb @@ -263,11 +263,15 @@ def commit(action, money, parameters) response = parse(ssl_post(url_for(action), post_data(action, money, parameters)), action) # Pass along the original transaction id in the case an update transaction - Response.new(response[:success], message_from(response, action), response, + Response.new( + response[:success], + message_from(response, action), + response, test: test?, authorization: response[:szTransactionFileName] || parameters[:szTransactionId], avs_result: { code: response[:szAVSResponseCode] }, - cvv_result: response[:szCVV2ResponseCode]) + cvv_result: response[:szCVV2ResponseCode] + ) end def url_for(action) diff --git a/lib/active_merchant/billing/gateways/smart_ps.rb b/lib/active_merchant/billing/gateways/smart_ps.rb index 1c63532b9e0..79d116be921 100644 --- a/lib/active_merchant/billing/gateways/smart_ps.rb +++ b/lib/active_merchant/billing/gateways/smart_ps.rb @@ -226,11 +226,15 @@ def parse(body) def commit(action, money, parameters) parameters[:amount] = localized_amount(money, parameters[:currency] || default_currency) if money response = parse(ssl_post(self.live_url, post_data(action, parameters))) - Response.new(response['response'] == '1', message_from(response), response, + Response.new( + response['response'] == '1', + message_from(response), + response, authorization: (response['transactionid'] || response['customer_vault_id']), test: test?, cvv_result: response['cvvresponse'], - avs_result: { code: response['avsresponse'] }) + avs_result: { code: response['avsresponse'] } + ) end def expdate(creditcard) diff --git a/lib/active_merchant/billing/gateways/so_easy_pay.rb b/lib/active_merchant/billing/gateways/so_easy_pay.rb index 16b5ef79297..f13a3e6d4cc 100644 --- a/lib/active_merchant/billing/gateways/so_easy_pay.rb +++ b/lib/active_merchant/billing/gateways/so_easy_pay.rb @@ -161,11 +161,13 @@ def commit(soap_action, soap, options) 'Content-Type' => 'text/xml; charset=utf-8' } response_string = ssl_post(test? ? self.test_url : self.live_url, soap, headers) response = parse(response_string, soap_action) - return Response.new(response['errorcode'] == '000', + return Response.new( + response['errorcode'] == '000', response['errormessage'], response, test: test?, - authorization: response['transaction_id']) + authorization: response['transaction_id'] + ) end def build_soap(request) diff --git a/lib/active_merchant/billing/gateways/stripe.rb b/lib/active_merchant/billing/gateways/stripe.rb index e9f527cdf47..da0eee50312 100644 --- a/lib/active_merchant/billing/gateways/stripe.rb +++ b/lib/active_merchant/billing/gateways/stripe.rb @@ -145,6 +145,7 @@ def capture(money, authorization, options = {}) def void(identification, options = {}) post = {} + post[:reverse_transfer] = options[:reverse_transfer] if options[:reverse_transfer] post[:metadata] = options[:metadata] if options[:metadata] post[:reason] = options[:reason] if options[:reason] post[:expand] = [:charge] @@ -280,6 +281,7 @@ def supports_scrubbing? def scrub(transcript) transcript. gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r((Authorization: Bearer )\w+), '\1[FILTERED]'). gsub(%r((&?three_d_secure\[cryptogram\]=)[\w=]*(&?)), '\1[FILTERED]\2'). gsub(%r(((\[card\]|card)\[cryptogram\]=)[^&]+(&?)), '\1[FILTERED]\3'). gsub(%r(((\[card\]|card)\[cvc\]=)\d+), '\1[FILTERED]'). @@ -301,7 +303,7 @@ def supports_network_tokenization? def delete_latest_test_external_account(account) return unless test? - auth_header = { 'Authorization': "Bearer #{options[:login]}" } + auth_header = { 'Authorization' => 'Basic ' + Base64.strict_encode64(options[:login].to_s + ':').strip } url = "#{live_url}accounts/#{CGI.escape(account)}/external_accounts" accounts_response = JSON.parse(ssl_get("#{url}?limit=100", auth_header)) to_delete = accounts_response['data'].reject { |ac| ac['default_for_currency'] } @@ -389,6 +391,7 @@ def add_charge_details(post, money, payment, options) end add_metadata(post, options) + add_shipping_address(post, payment, options) add_application_fee(post, options) add_exchange_rate(post, options) add_destination(post, options) @@ -555,6 +558,23 @@ def add_emv_metadata(post, creditcard) post[:metadata][:card_read_method] = creditcard.read_method if creditcard.respond_to?(:read_method) end + def add_shipping_address(post, payment, options = {}) + return unless shipping = options[:shipping_address] + return unless shipping_name = shipping[:name] + + post[:shipping] = {} + + post[:shipping][:name] = shipping_name + post[:shipping][:address] = {} + post[:shipping][:address][:line1] = shipping[:address1] + post[:shipping][:address][:line2] = shipping[:address2] if shipping[:address2] + post[:shipping][:address][:city] = shipping[:city] if shipping[:city] + post[:shipping][:address][:country] = shipping[:country] if shipping[:country] + post[:shipping][:address][:state] = shipping[:state] if shipping[:state] + post[:shipping][:address][:postal_code] = shipping[:zip] if shipping[:zip] + post[:shipping][:phone] = shipping[:phone_number] if shipping[:phone_number] + end + def add_source_owner(post, creditcard, options) post[:owner] = {} post[:owner][:name] = creditcard.name if creditcard.name @@ -632,18 +652,19 @@ def flatten_array(flattened, array, prefix) end end - def headers(options = {}) - key = options[:key] || @api_key - idempotency_key = options[:idempotency_key] + def key(options = {}) + options[:key] || @api_key + end + def headers(options = {}) headers = { - 'Authorization' => 'Basic ' + Base64.strict_encode64(key.to_s + ':').strip, + 'Authorization' => 'Basic ' + Base64.strict_encode64(key(options).to_s + ':').strip, 'User-Agent' => "Stripe/v1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}", 'Stripe-Version' => api_version(options), 'X-Stripe-Client-User-Agent' => stripe_client_user_agent(options), 'X-Stripe-Client-User-Metadata' => { ip: options[:ip] }.to_json } - headers['Idempotency-Key'] = idempotency_key if idempotency_key + headers['Idempotency-Key'] = options[:idempotency_key] if options[:idempotency_key] headers['Stripe-Account'] = options[:stripe_account] if options[:stripe_account] headers end @@ -674,6 +695,9 @@ def api_request(method, endpoint, parameters = nil, options = {}) def commit(method, url, parameters = nil, options = {}) add_expand_parameters(parameters, options) if parameters + + return Response.new(false, 'Invalid API Key provided') unless key_valid?(options) + response = api_request(method, url, parameters, options) response['webhook_id'] = options[:webhook_id] if options[:webhook_id] success = success_from(response, options) @@ -681,7 +705,8 @@ def commit(method, url, parameters = nil, options = {}) card = card_from_response(response) avs_code = AVS_CODE_TRANSLATOR["line1: #{card['address_line1_check']}, zip: #{card['address_zip_check']}"] cvc_code = CVC_CODE_TRANSLATOR[card['cvc_check']] - Response.new(success, + Response.new( + success, message_from(success, response), response, test: response_is_test?(response), @@ -689,7 +714,20 @@ def commit(method, url, parameters = nil, options = {}) avs_result: { code: avs_code }, cvv_result: cvc_code, emv_authorization: emv_authorization_from_response(response), - error_code: success ? nil : error_code_from(response)) + error_code: success ? nil : error_code_from(response) + ) + end + + def key_valid?(options) + return true unless test? + + %w(sk rk).each do |k| + if key(options).start_with?(k) + return false unless key(options).start_with?("#{k}_test") + end + end + + true end def authorization_from(success, url, method, response) diff --git a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb index 81b0caced1d..f423ff39aa5 100644 --- a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +++ b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb @@ -14,7 +14,7 @@ class StripePaymentIntentsGateway < StripeGateway def create_intent(money, payment_method, options = {}) MultiResponse.run do |r| - if payment_method.is_a?(NetworkTokenizationCreditCard) + if payment_method.is_a?(NetworkTokenizationCreditCard) && digital_wallet_payment_method?(payment_method) r.process { tokenize_apple_google(payment_method, options) } payment_method = (r.params['token']['id']) if r.success? end @@ -28,20 +28,23 @@ def create_intent(money, payment_method, options = {}) result = add_payment_method_token(post, payment_method, options) return result if result.is_a?(ActiveMerchant::Billing::Response) + add_network_token_cryptogram_and_eci(post, payment_method) add_external_three_d_secure_auth_data(post, options) add_metadata(post, options) add_return_url(post, options) add_connected_account(post, options) add_radar_data(post, options) add_shipping_address(post, options) + add_stored_credentials(post, options) setup_future_usage(post, options) add_exemption(post, options) - add_stored_credentials(post, options) add_ntid(post, options) add_claim_without_transaction_id(post, options) add_error_on_requires_action(post, options) add_fulfillment_date(post, options) request_three_d_secure(post, options) + add_level_three(post, options) + post[:expand] = ['charges.data.balance_transaction'] CREATE_INTENT_ATTRIBUTES.each do |attribute| add_whitelisted_attribute(post, options, attribute) @@ -55,6 +58,11 @@ def show_intent(intent_id, options) commit(:get, "payment_intents/#{intent_id}", nil, options) end + def create_test_customer + response = api_request(:post, 'customers') + response['id'] + end + def confirm_intent(intent_id, payment_method, options = {}) post = {} result = add_payment_method_token(post, payment_method, options) @@ -70,22 +78,27 @@ def confirm_intent(intent_id, payment_method, options = {}) def create_payment_method(payment_method, options = {}) post_data = add_payment_method_data(payment_method, options) - options = format_idempotency_key(options, 'pm') commit(:post, 'payment_methods', post_data, options) end def add_payment_method_data(payment_method, options = {}) - post_data = {} - post_data[:type] = 'card' - post_data[:card] = {} - post_data[:card][:number] = payment_method.number - post_data[:card][:exp_month] = payment_method.month - post_data[:card][:exp_year] = payment_method.year - post_data[:card][:cvc] = payment_method.verification_value if payment_method.verification_value - add_billing_address(post_data, options) - add_name_only(post_data, payment_method) if post_data[:billing_details].nil? - post_data + post = { + type: 'card', + card: { + exp_month: payment_method.month, + exp_year: payment_method.year + } + } + post[:card][:number] = payment_method.number unless adding_network_token_card_data?(payment_method) + post[:card][:cvc] = payment_method.verification_value if payment_method.verification_value + if billing = options[:billing_address] || options[:address] + post[:billing_details] = add_address(billing, options) + end + + add_name_only(post, payment_method) if post[:billing_details].nil? + add_network_token_data(post, payment_method, options) + post end def add_payment_method_card_data_token(post_data, payment_method) @@ -116,23 +129,27 @@ def update_intent(money, intent_id, payment_method, options = {}) end def create_setup_intent(payment_method, options = {}) - post = {} - add_customer(post, options) - result = add_payment_method_token(post, payment_method, options) - return result if result.is_a?(ActiveMerchant::Billing::Response) + MultiResponse.run do |r| + r.process do + post = {} + add_customer(post, options) + result = add_payment_method_token(post, payment_method, options, r) + return result if result.is_a?(ActiveMerchant::Billing::Response) - add_metadata(post, options) - add_return_url(post, options) - add_fulfillment_date(post, options) - request_three_d_secure(post, options) - post[:on_behalf_of] = options[:on_behalf_of] if options[:on_behalf_of] - post[:usage] = options[:usage] if %w(on_session off_session).include?(options[:usage]) - post[:description] = options[:description] if options[:description] + add_metadata(post, options) + add_return_url(post, options) + add_fulfillment_date(post, options) + request_three_d_secure(post, options) + post[:on_behalf_of] = options[:on_behalf_of] if options[:on_behalf_of] + post[:usage] = options[:usage] if %w(on_session off_session).include?(options[:usage]) + post[:description] = options[:description] if options[:description] - commit(:post, 'setup_intents', post, options) + commit(:post, 'setup_intents', post, options) + end + end end - def retrieve_setup_intent(setup_intent_id) + def retrieve_setup_intent(setup_intent_id, options = {}) # Retrieving a setup_intent passing 'expand[]=latest_attempt' allows the caller to # check for a network_transaction_id and ds_transaction_id # eg (latest_attempt -> payment_method_details -> card -> network_transaction_id) @@ -140,7 +157,7 @@ def retrieve_setup_intent(setup_intent_id) # Being able to retrieve these fields enables payment flows that rely on MIT exemptions, e.g: off_session commit(:post, "setup_intents/#{setup_intent_id}", { 'expand[]': 'latest_attempt' - }, {}) + }, options) end def authorize(money, payment_method, options = {}) @@ -198,20 +215,11 @@ def store(payment_method, options = {}) post = {} # If customer option is provided, create a payment method and attach to customer id # Otherwise, create a customer, then attach - if payment_method.is_a?(StripePaymentToken) || payment_method.is_a?(ActiveMerchant::Billing::CreditCard) || (payment_method.is_a?(String) && payment_method.starts_with?("pm_")) + if payment_method.is_a?(StripePaymentToken) || payment_method.is_a?(ActiveMerchant::Billing::CreditCard) result = add_payment_method_token(params, payment_method, options) return result if result.is_a?(ActiveMerchant::Billing::Response) - if options[:customer] - customer_id = options[:customer] - else - post[:description] = options[:description] if options[:description] - post[:email] = options[:email] if options[:email] - options = format_idempotency_key(options, 'customer') - post[:expand] = [:sources] - customer = commit(:post, 'customers', post, options) - customer_id = customer.params['id'] - end + customer_id = options[:customer] || customer(post, payment_method, options).params['id'] options = format_idempotency_key(options, 'attach') attach_parameters = { customer: customer_id } attach_parameters[:validate] = options[:validate] unless options[:validate].nil? @@ -221,6 +229,23 @@ def store(payment_method, options = {}) end end + def customer(post, payment, options) + post[:description] = options[:description] if options[:description] + post[:expand] = [:sources] + post[:email] = options[:email] + + if billing = options[:billing_address] || options[:address] + post.merge!(add_address(billing, options)) + end + + if shipping = options[:shipping_address] + post[:shipping] = add_address(shipping, options).except(:email) + end + + options = format_idempotency_key(options, 'customer') + commit(:post, 'customers', post, options) + end + def unstore(identification, options = {}, deprecated_options = {}) if identification.include?('pm_') _, payment_method = identification.split('|') @@ -231,7 +256,7 @@ def unstore(identification, options = {}, deprecated_options = {}) end def verify(payment_method, options = {}) - create_setup_intent(payment_method, options.merge!(confirm: true)) + create_setup_intent(payment_method, options.merge!({ confirm: true, verify: true })) end def setup_purchase(money, options = {}) @@ -244,8 +269,22 @@ def setup_purchase(money, options = {}) commit(:post, 'payment_intents', post, options) end + def supports_network_tokenization? + true + end + private + def digital_wallet_payment_method?(payment_method) + payment_method.source == :google_pay || payment_method.source == :apple_pay + end + + def adding_network_token_card_data?(payment_method) + return true if payment_method.is_a?(ActiveMerchant::Billing::NetworkTokenizationCreditCard) && payment_method.source == :network_token + + false + end + def off_session_request?(options = {}) (options[:off_session] || options[:setup_future_usage]) && options[:confirm] == true end @@ -284,6 +323,19 @@ def add_metadata(post, options = {}) post[:metadata][:event_type] = options[:event_type] if options[:event_type] end + def add_level_three(post, options = {}) + level_three = {} + + level_three[:merchant_reference] = options[:merchant_reference] if options[:merchant_reference] + level_three[:customer_reference] = options[:customer_reference] if options[:customer_reference] + level_three[:shipping_address_zip] = options[:shipping_address_zip] if options[:shipping_address_zip] + level_three[:shipping_from_zip] = options[:shipping_from_zip] if options[:shipping_from_zip] + level_three[:shipping_amount] = options[:shipping_amount] if options[:shipping_amount] + level_three[:line_items] = options[:line_items] if options[:line_items] + + post[:level3] = level_three unless level_three.empty? + end + def add_return_url(post, options) return unless options[:confirm] @@ -291,7 +343,7 @@ def add_return_url(post, options) post[:return_url] = options[:return_url] if options[:return_url] end - def add_payment_method_token(post, payment_method, options) + def add_payment_method_token(post, payment_method, options, responses = []) case payment_method when StripePaymentToken post[:payment_method_data] = { @@ -304,10 +356,38 @@ def add_payment_method_token(post, payment_method, options) when String extract_token_from_string_and_maybe_add_customer_id(post, payment_method) when ActiveMerchant::Billing::CreditCard - get_payment_method_data_from_card(post, payment_method, options) + return create_payment_method_and_extract_token(post, payment_method, options, responses) if options[:verify] + + get_payment_method_data_from_card(post, payment_method, options, responses) + when ActiveMerchant::Billing::NetworkTokenizationCreditCard + get_payment_method_data_from_card(post, payment_method, options, responses) end end + def add_network_token_data(post_data, payment_method, options) + return unless adding_network_token_card_data?(payment_method) + + post_data[:card] ||= {} + post_data[:card][:last4] = options[:last_4] + post_data[:card][:network_token] = {} + post_data[:card][:network_token][:number] = payment_method.number + post_data[:card][:network_token][:exp_month] = payment_method.month + post_data[:card][:network_token][:exp_year] = payment_method.year + post_data[:card][:network_token][:payment_account_reference] = options[:payment_account_reference] if options[:payment_account_reference] + + post_data + end + + def add_network_token_cryptogram_and_eci(post, payment_method) + return unless adding_network_token_card_data?(payment_method) + + post[:payment_method_options] ||= {} + post[:payment_method_options][:card] ||= {} + post[:payment_method_options][:card][:network_token] ||= {} + post[:payment_method_options][:card][:network_token][:cryptogram] = payment_method.payment_cryptogram if payment_method.payment_cryptogram + post[:payment_method_options][:card][:network_token][:electronic_commerce_indicator] = payment_method.eci if payment_method.eci + end + def extract_token_from_string_and_maybe_add_customer_id(post, payment_method) if payment_method.include?('|') customer_id, payment_method = payment_method.split('|') @@ -333,6 +413,7 @@ def tokenize_apple_google(payment, options = {}) cryptogram: payment.payment_cryptogram } } + add_billing_address_for_card_tokenization(post, options) if %i(apple_pay android_pay).include?(tokenization_method) token_response = api_request(:post, 'tokens', post, options) success = token_response['error'].nil? if success && token_response['id'] @@ -344,16 +425,17 @@ def tokenize_apple_google(payment, options = {}) end end - def get_payment_method_data_from_card(post, payment_method, options) - return create_payment_method_and_extract_token(post, payment_method, options) unless off_session_request?(options) + def get_payment_method_data_from_card(post, payment_method, options, responses) + return create_payment_method_and_extract_token(post, payment_method, options, responses) unless off_session_request?(options) || adding_network_token_card_data?(payment_method) post[:payment_method_data] = add_payment_method_data(payment_method, options) end - def create_payment_method_and_extract_token(post, payment_method, options) + def create_payment_method_and_extract_token(post, payment_method, options, responses) payment_method_response = create_payment_method(payment_method, options) return payment_method_response if payment_method_response.failure? + responses << payment_method_response add_payment_method_token(post, payment_method_response.params['id'], options) end @@ -372,24 +454,80 @@ def add_exemption(post, options = {}) post[:payment_method_options][:card][:moto] = true if options[:moto] end - # Stripe Payment Intents does not pass any parameters for cardholder/merchant initiated - # it also does not support installments for any country other than Mexico (reason for this is unknown) - # The only thing that Stripe PI requires for stored credentials to work currently is the network_transaction_id - # network_transaction_id is created when the card is authenticated using the field `setup_for_future_usage` with the value `off_session` see def setup_future_usage below + # Stripe Payment Intents now supports specifying on a transaction level basis stored credential information. + # The feature is currently gated but is listed as `stored_credential_transaction_type` inside the + # `post[:payment_method_options][:card]` hash. Since this is a beta field adding an extra check to use + # the existing logic by default. To be able to utilize this field, you must reach out to Stripe. def add_stored_credentials(post, options = {}) - return unless options[:stored_credential] && !options[:stored_credential].values.all?(&:nil?) - stored_credential = options[:stored_credential] + return unless stored_credential && !stored_credential.values.all?(&:nil?) + post[:payment_method_options] ||= {} post[:payment_method_options][:card] ||= {} - post[:payment_method_options][:card][:mit_exemption] = {} + + card_options = post[:payment_method_options][:card] + card_options[:mit_exemption] = {} # Stripe PI accepts network_transaction_id and ds_transaction_id via mit field under card. # The network_transaction_id can be sent in nested under stored credentials OR as its own field (add_ntid handles when it is sent in on its own) # If it is sent is as its own field AND under stored credentials, the value sent under its own field is what will send. - post[:payment_method_options][:card][:mit_exemption][:ds_transaction_id] = stored_credential[:ds_transaction_id] if stored_credential[:ds_transaction_id] - post[:payment_method_options][:card][:mit_exemption][:network_transaction_id] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id] + card_options[:mit_exemption][:ds_transaction_id] = stored_credential[:ds_transaction_id] if stored_credential[:ds_transaction_id] + unless options[:setup_future_usage] == 'off_session' + card_options[:mit_exemption][:network_transaction_id] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id] + end + + add_stored_credential_transaction_type(post, options) + end + + def add_stored_credential_transaction_type(post, options = {}) + return unless options[:stored_credential_transaction_type] + + stored_credential = options[:stored_credential] + # Do not add anything unless these are present. + return unless stored_credential[:reason_type] && stored_credential[:initiator] + + # Not compatible with off_session parameter. + options.delete(:off_session) + + stored_credential_type = if stored_credential[:initial_transaction] + return unless stored_credential[:initiator] == 'cardholder' + + initial_transaction_stored_credential(post, stored_credential) + else + subsequent_transaction_stored_credential(post, stored_credential) + end + + card_options = post[:payment_method_options][:card] + card_options[:stored_credential_transaction_type] = stored_credential_type + card_options[:mit_exemption].delete(:network_transaction_id) if stored_credential_type == 'setup_on_session' + end + + def initial_transaction_stored_credential(post, stored_credential) + case stored_credential[:reason_type] + when 'unscheduled' + # Charge on-session and store card for future one-off payment use + 'setup_off_session_unscheduled' + when 'recurring' + # Charge on-session and store card for future recurring payment use + 'setup_off_session_recurring' + else + # Charge on-session and store card for future on-session payment use. + 'setup_on_session' + end + end + + def subsequent_transaction_stored_credential(post, stored_credential) + if stored_credential[:initiator] == 'cardholder' + # Charge on-session customer using previously stored card. + 'stored_on_session' + elsif stored_credential[:reason_type] == 'recurring' + # Charge off-session customer using previously stored card for recurring transaction + 'stored_off_session_recurring' + else + # Charge off-session customer using previously stored card for one-off transaction + 'stored_off_session_unscheduled' + end end def add_ntid(post, options = {}) @@ -399,7 +537,7 @@ def add_ntid(post, options = {}) post[:payment_method_options][:card] ||= {} post[:payment_method_options][:card][:mit_exemption] = {} - post[:payment_method_options][:card][:mit_exemption][:network_transaction_id] = options[:network_transaction_id] if options[:network_transaction_id] + post[:payment_method_options][:card][:mit_exemption][:network_transaction_id] = options[:network_transaction_id] end def add_claim_without_transaction_id(post, options = {}) @@ -448,50 +586,47 @@ def setup_future_usage(post, options = {}) post end - def add_billing_address(post, options = {}) - return unless billing = options[:billing_address] || options[:address] - - post[:billing_details] = {} - post[:billing_details][:address] = {} - post[:billing_details][:address][:city] = billing[:city] if billing[:city] - post[:billing_details][:address][:country] = billing[:country] if billing[:country] - post[:billing_details][:address][:line1] = billing[:address1] if billing[:address1] - post[:billing_details][:address][:line2] = billing[:address2] if billing[:address2] - post[:billing_details][:address][:postal_code] = billing[:zip] if billing[:zip] - post[:billing_details][:address][:state] = billing[:state] if billing[:state] - post[:billing_details][:email] = billing[:email] if billing[:email] - post[:billing_details][:name] = billing[:name] if billing[:name] - post[:billing_details][:phone] = billing[:phone] if billing[:phone] - end + def add_billing_address_for_card_tokenization(post, options = {}) + return unless (billing = options[:billing_address] || options[:address]) - def add_name_only(post, payment_method) - post[:billing_details] = {} unless post[:billing_details] + billing = add_address(billing, options) + billing[:address].transform_keys! { |k| k == :postal_code ? :address_zip : k.to_s.prepend('address_').to_sym } - name = [payment_method.first_name, payment_method.last_name].compact.join(' ') - post[:billing_details][:name] = name + post[:card][:name] = billing[:name] + post[:card].merge!(billing[:address]) end def add_shipping_address(post, options = {}) return unless shipping = options[:shipping_address] - post[:shipping] = {} - - # fields required by Stripe PI - post[:shipping][:address] = {} - post[:shipping][:address][:line1] = shipping[:address1] - post[:shipping][:name] = shipping[:name] - - # fields considered optional by Stripe PI - post[:shipping][:address][:city] = shipping[:city] if shipping[:city] - post[:shipping][:address][:country] = shipping[:country] if shipping[:country] - post[:shipping][:address][:line2] = shipping[:address2] if shipping[:address2] - post[:shipping][:address][:postal_code] = shipping[:zip] if shipping[:zip] - post[:shipping][:address][:state] = shipping[:state] if shipping[:state] - post[:shipping][:phone] = shipping[:phone_number] if shipping[:phone_number] + post[:shipping] = add_address(shipping, options).except(:email) post[:shipping][:carrier] = (shipping[:carrier] || options[:shipping_carrier]) if shipping[:carrier] || options[:shipping_carrier] post[:shipping][:tracking_number] = (shipping[:tracking_number] || options[:shipping_tracking_number]) if shipping[:tracking_number] || options[:shipping_tracking_number] end + def add_address(address, options) + { + address: { + city: address[:city], + country: address[:country], + line1: address[:address1], + line2: address[:address2], + postal_code: address[:zip], + state: address[:state] + }.compact, + email: address[:email] || options[:email], + phone: address[:phone] || address[:phone_number], + name: address[:name] + }.compact + end + + def add_name_only(post, payment_method) + post[:billing_details] = {} unless post[:billing_details] + + name = [payment_method.first_name, payment_method.last_name].compact.join(' ') + post[:billing_details][:name] = name + end + def format_idempotency_key(options, suffix) return options unless options[:idempotency_key] @@ -501,7 +636,7 @@ def format_idempotency_key(options, suffix) def success_from(response, options) if response['status'] == 'requires_action' && !options[:execute_threed] response['error'] = {} - response['error']['message'] = 'Received unexpected 3DS authentication response. Use the execute_threed option to initiate a proper 3DS flow.' + response['error']['message'] = 'Received unexpected 3DS authentication response, but a 3DS initiation flag was not included in the request.' return false end diff --git a/lib/active_merchant/billing/gateways/sum_up.rb b/lib/active_merchant/billing/gateways/sum_up.rb new file mode 100644 index 00000000000..c2824c9f39f --- /dev/null +++ b/lib/active_merchant/billing/gateways/sum_up.rb @@ -0,0 +1,205 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class SumUpGateway < Gateway + self.live_url = 'https://api.sumup.com/v0.1/' + + self.supported_countries = %w(AT BE BG BR CH CL CO CY CZ DE DK EE ES FI FR + GB GR HR HU IE IT LT LU LV MT NL NO PL PT RO + SE SI SK US) + self.currencies_with_three_decimal_places = %w(EUR BGN BRL CHF CZK DKK GBP + HUF NOK PLN SEK USD) + self.default_currency = 'USD' + + self.homepage_url = 'https://www.sumup.com/' + self.display_name = 'SumUp' + + STANDARD_ERROR_CODE_MAPPING = { + multiple_invalid_parameters: 'MULTIPLE_INVALID_PARAMETERS' + } + + def initialize(options = {}) + requires!(options, :access_token, :pay_to_email) + super + end + + def purchase(money, payment, options = {}) + MultiResponse.run do |r| + r.process { create_checkout(money, payment, options) } unless options[:checkout_id] + r.process { complete_checkout(options[:checkout_id] || r.params['id'], payment, options) } + end + end + + def void(authorization, options = {}) + checkout_id = authorization.split('#')[0] + commit('checkouts/' + checkout_id, {}, :delete) + end + + def refund(money, authorization, options = {}) + transaction_id = authorization.split('#')[-1] + payment_currency = options[:currency] || currency(money) + post = money ? { amount: localized_amount(money, payment_currency) } : {} + add_merchant_data(post, options) + + commit('me/refund/' + transaction_id, post) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Bearer )\w+), '\1[FILTERED]'). + gsub(%r(("pay_to_email\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cvv\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]') + end + + private + + def create_checkout(money, payment, options) + post = {} + + add_merchant_data(post, options) + add_invoice(post, money, options) + add_address(post, options) + add_customer_data(post, payment, options) + + commit('checkouts', post) + end + + def complete_checkout(checkout_id, payment, options = {}) + post = {} + + add_payment(post, payment, options) + + commit('checkouts/' + checkout_id, post, :put) + end + + def add_customer_data(post, payment, options) + post[:customer_id] = options[:customer_id] + post[:personal_details] = { + email: options[:email], + first_name: payment&.first_name, + last_name: payment&.last_name, + tax_id: options[:tax_id] + } + end + + def add_merchant_data(post, options) + # Required field: pay_to_email + # Description: Email address of the merchant to whom the payment is made. + post[:pay_to_email] = @options[:pay_to_email] + end + + def add_address(post, options) + post[:personal_details] ||= {} + if address = (options[:billing_address] || options[:shipping_address] || options[:address]) + post[:personal_details][:address] = { + city: address[:city], + state: address[:state], + country: address[:country], + line_1: address[:address1], + postal_code: address[:zip] + } + end + end + + def add_invoice(post, money, options) + payment_currency = options[:currency] || currency(money) + post[:checkout_reference] = options[:order_id] + post[:amount] = localized_amount(money, payment_currency) + post[:currency] = payment_currency + post[:description] = options[:description] + end + + def add_payment(post, payment, options) + post[:payment_type] = options[:payment_type] || 'card' + + post[:card] = { + name: payment.name, + number: payment.number, + expiry_month: format(payment.month, :two_digits), + expiry_year: payment.year, + cvv: payment.verification_value + } + end + + def commit(action, post, method = :post) + response = api_request(action, post.compact, method) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + test: test?, + error_code: error_code_from(response) + ) + end + + def api_request(action, post, method) + begin + raw_response = ssl_request(method, live_url + action, post.to_json, auth_headers) + rescue ResponseError => e + raw_response = e.response.body + end + + response = parse(raw_response) + # Multiple invalid parameters + response = format_multiple_errors(response) if raw_response.include?('error_code') && response.is_a?(Array) + + return response.symbolize_keys + end + + def parse(body) + JSON.parse(body) + end + + def success_from(response) + return false unless %w(PENDING EXPIRED PAID).include?(response[:status]) + + response[:transactions].each do |transaction| + return false unless %w(PENDING CANCELLED SUCCESSFUL).include?(transaction.symbolize_keys[:status]) + end + + true + end + + def message_from(response) + return response[:status] if success_from(response) + + response[:message] || response[:error_message] + end + + def authorization_from(response) + return response[:id] unless response[:transaction_id] + + [response[:id], response[:transaction_id]].join('#') + end + + def auth_headers + { + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer #{options[:access_token]}" + } + end + + def error_code_from(response) + response[:error_code] unless success_from(response) + end + + def format_multiple_errors(responses) + errors = responses.map do |response| + { error_code: response['error_code'], param: response['param'] } + end + + { + error_code: STANDARD_ERROR_CODE_MAPPING[:multiple_invalid_parameters], + message: 'Validation error', + errors: errors + } + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/swipe_checkout.rb b/lib/active_merchant/billing/gateways/swipe_checkout.rb index f80621d0233..e274ce2d918 100644 --- a/lib/active_merchant/billing/gateways/swipe_checkout.rb +++ b/lib/active_merchant/billing/gateways/swipe_checkout.rb @@ -104,12 +104,14 @@ def commit(action, money, parameters) result = response['data']['result'] success = (result == 'accepted' || (test? && result == 'test-accepted')) - Response.new(success, + Response.new( + success, success ? TRANSACTION_APPROVED_MSG : TRANSACTION_DECLINED_MSG, response, - test: test?) + test: test? + ) else build_error_response(message, response) end diff --git a/lib/active_merchant/billing/gateways/tns.rb b/lib/active_merchant/billing/gateways/tns.rb index 15c47eadb82..b84da916ef5 100644 --- a/lib/active_merchant/billing/gateways/tns.rb +++ b/lib/active_merchant/billing/gateways/tns.rb @@ -8,13 +8,10 @@ class TnsGateway < Gateway VERSION = '52' self.live_na_url = "https://secure.na.tnspayments.com/api/rest/version/#{VERSION}/" - self.test_na_url = "https://secure.na.tnspayments.com/api/rest/version/#{VERSION}/" - self.live_ap_url = "https://secure.ap.tnspayments.com/api/rest/version/#{VERSION}/" - self.test_ap_url = "https://secure.ap.tnspayments.com/api/rest/version/#{VERSION}/" - self.live_eu_url = "https://secure.eu.tnspayments.com/api/rest/version/#{VERSION}/" - self.test_eu_url = "https://secure.eu.tnspayments.com/api/rest/version/#{VERSION}/" + + self.test_url = "https://secure.uat.tnspayments.com/api/rest/version/#{VERSION}/" self.display_name = 'TNS' self.homepage_url = 'http://www.tnsi.com/' diff --git a/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb b/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb index 3d68c068422..36a5d43084d 100644 --- a/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb +++ b/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb @@ -482,7 +482,7 @@ def add_transaction_code_to_request(request, action) doc = Nokogiri::XML::Document.parse(request) merc_nodeset = doc.xpath('//v1:merc', 'v1' => V1_NAMESPACE) - merc_nodeset.after "#{TRANSACTION_CODES[action]}" + merc_nodeset.after "#{TRANSACTION_CODES[action]}" doc.root.to_xml end diff --git a/lib/active_merchant/billing/gateways/trust_commerce.rb b/lib/active_merchant/billing/gateways/trust_commerce.rb index 3b805e94657..88529d90c5d 100644 --- a/lib/active_merchant/billing/gateways/trust_commerce.rb +++ b/lib/active_merchant/billing/gateways/trust_commerce.rb @@ -248,6 +248,12 @@ def void(authorization, options = {}) commit(action, parameters) end + def verify(credit_card, options = {}) + parameters = {} + add_creditcard(parameters, credit_card) + commit('verify', parameters) + end + # recurring() a TrustCommerce account that is activated for Citadel, TrustCommerce's # hosted customer billing info database. # @@ -444,11 +450,15 @@ def commit(action, parameters) # to be considered successful, transaction status must be either "approved" or "accepted" success = SUCCESS_TYPES.include?(data['status']) message = message_from(data) - Response.new(success, message, data, + Response.new( + success, + message, + data, test: test?, authorization: authorization_from(action, data), cvv_result: data['cvv'], - avs_result: { code: data['avs'] }) + avs_result: { code: data['avs'] } + ) end def parse(body) @@ -476,9 +486,14 @@ def message_from(data) end def authorization_from(action, data) - authorization = data['transid'] - authorization = "#{authorization}|#{action}" if authorization && VOIDABLE_ACTIONS.include?(action) - authorization + case action + when 'store' + data['billingid'] + when *VOIDABLE_ACTIONS + "#{data['transid']}|#{action}" + else + data['transid'] + end end def split_authorization(authorization) diff --git a/lib/active_merchant/billing/gateways/usa_epay_advanced.rb b/lib/active_merchant/billing/gateways/usa_epay_advanced.rb index 20a985a4fdd..dd16d383583 100644 --- a/lib/active_merchant/billing/gateways/usa_epay_advanced.rb +++ b/lib/active_merchant/billing/gateways/usa_epay_advanced.rb @@ -1030,15 +1030,17 @@ def get_account_details # Build soap header, etc. def build_request(action, options = {}) - soap = Builder::XmlMarkup.new - soap.instruct!(:xml, version: '1.0', encoding: 'utf-8') - soap.tag! 'SOAP-ENV:Envelope', + envelope_obj = { 'xmlns:SOAP-ENV' => 'http://schemas.xmlsoap.org/soap/envelope/', 'xmlns:ns1' => 'urn:usaepay', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:SOAP-ENC' => 'http://schemas.xmlsoap.org/soap/encoding/', - 'SOAP-ENV:encodingStyle' => 'http://schemas.xmlsoap.org/soap/encoding/' do + 'SOAP-ENV:encodingStyle' => 'http://schemas.xmlsoap.org/soap/encoding/' + } + soap = Builder::XmlMarkup.new + soap.instruct!(:xml, version: '1.0', encoding: 'utf-8') + soap.tag! 'SOAP-ENV:Envelope', envelope_obj do soap.tag! 'SOAP-ENV:Body' do send("build_#{action}", soap, options) end @@ -1335,8 +1337,7 @@ def build_credit_card_or_check(soap, payment_method) case when payment_method[:method].kind_of?(ActiveMerchant::Billing::CreditCard) build_tag soap, :string, 'CardNumber', payment_method[:method].number - build_tag soap, :string, 'CardExpiration', - "#{'%02d' % payment_method[:method].month}#{payment_method[:method].year.to_s[-2..-1]}" + build_tag soap, :string, 'CardExpiration', "#{'%02d' % payment_method[:method].month}#{payment_method[:method].year.to_s[-2..-1]}" if options[:billing_address] build_tag soap, :string, 'AvsStreet', options[:billing_address][:address1] build_tag soap, :string, 'AvsZip', options[:billing_address][:zip] @@ -1520,8 +1521,8 @@ def commit(action, request) begin soap = ssl_post(url, request, 'Content-Type' => 'text/xml') - rescue ActiveMerchant::ResponseError => error - soap = error.response.body + rescue ActiveMerchant::ResponseError => e + soap = e.response.body end build_response(action, soap) diff --git a/lib/active_merchant/billing/gateways/usa_epay_transaction.rb b/lib/active_merchant/billing/gateways/usa_epay_transaction.rb index c012758b305..1faa8291fb2 100644 --- a/lib/active_merchant/billing/gateways/usa_epay_transaction.rb +++ b/lib/active_merchant/billing/gateways/usa_epay_transaction.rb @@ -329,12 +329,16 @@ def commit(action, parameters) approved = response[:status] == 'Approved' error_code = nil error_code = (STANDARD_ERROR_CODE_MAPPING[response[:error_code]] || STANDARD_ERROR_CODE[:processing_error]) unless approved - Response.new(approved, message_from(response), response, + Response.new( + approved, + message_from(response), + response, test: test?, authorization: authorization_from(action, response), cvv_result: response[:cvv2_result_code], avs_result: { code: response[:avs_result_code] }, - error_code: error_code) + error_code: error_code + ) end def message_from(response) diff --git a/lib/active_merchant/billing/gateways/vanco.rb b/lib/active_merchant/billing/gateways/vanco.rb index 03ae8a2464d..f909e84f55d 100644 --- a/lib/active_merchant/billing/gateways/vanco.rb +++ b/lib/active_merchant/billing/gateways/vanco.rb @@ -15,15 +15,24 @@ class VancoGateway < Gateway self.homepage_url = 'http://vancopayments.com/' self.display_name = 'Vanco Payment Solutions' + SECONDS_PER_DAY = 3600 * 24 + BUFFER_TIME_IN_SECS = 60 * 3 + def initialize(options = {}) requires!(options, :user_id, :password, :client_id) super end def purchase(money, payment_method, options = {}) - MultiResponse.run do |r| - r.process { login } - r.process { commit(purchase_request(money, payment_method, r.params['response_sessionid'], options), :response_transactionref) } + moment_less_than_24_hours_ago = Time.now - SECONDS_PER_DAY - BUFFER_TIME_IN_SECS + + if options[:session_id] && options[:session_id][:created_at] >= moment_less_than_24_hours_ago + commit(purchase_request(money, payment_method, options[:session_id][:id], options), :response_transactionref) + else + MultiResponse.run do |r| + r.process { login } + r.process { commit(purchase_request(money, payment_method, r.params['response_sessionid'], options), :response_transactionref) } + end end end diff --git a/lib/active_merchant/billing/gateways/verifi.rb b/lib/active_merchant/billing/gateways/verifi.rb index a11a6fcc279..5df036a5a77 100644 --- a/lib/active_merchant/billing/gateways/verifi.rb +++ b/lib/active_merchant/billing/gateways/verifi.rb @@ -194,11 +194,15 @@ def commit(trx_type, money, post) response = parse(ssl_post(self.live_url, post_data(trx_type, post))) - Response.new(response[:response].to_i == SUCCESS, message_from(response), response, + Response.new( + response[:response].to_i == SUCCESS, + message_from(response), + response, test: test?, authorization: response[:transactionid], avs_result: { code: response[:avsresponse] }, - cvv_result: response[:cvvresponse]) + cvv_result: response[:cvvresponse] + ) end def message_from(response) diff --git a/lib/active_merchant/billing/gateways/viaklix.rb b/lib/active_merchant/billing/gateways/viaklix.rb index 76bfc8e46ca..dd49530a77f 100644 --- a/lib/active_merchant/billing/gateways/viaklix.rb +++ b/lib/active_merchant/billing/gateways/viaklix.rb @@ -136,11 +136,15 @@ def commit(action, money, parameters) response = parse(ssl_post(test? ? self.test_url : self.live_url, post_data(parameters))) - Response.new(response['result'] == APPROVED, message_from(response), response, + Response.new( + response['result'] == APPROVED, + message_from(response), + response, test: @options[:test] || test?, authorization: authorization_from(response), avs_result: { code: response['avs_response'] }, - cvv_result: response['cvv2_response']) + cvv_result: response['cvv2_response'] + ) end def authorization_from(response) diff --git a/lib/active_merchant/billing/gateways/visanet_peru.rb b/lib/active_merchant/billing/gateways/visanet_peru.rb index 75c429f0c96..6bd87e406c1 100644 --- a/lib/active_merchant/billing/gateways/visanet_peru.rb +++ b/lib/active_merchant/billing/gateways/visanet_peru.rb @@ -143,7 +143,7 @@ def split_authorization(authorization) end def generate_purchase_number_stamp - (Time.now.to_f.round(2) * 100).to_i.to_s + rand(('9' * 12).to_i).to_s.center(12, rand(9).to_s) end def commit(action, params, options = {}) diff --git a/lib/active_merchant/billing/gateways/vpos.rb b/lib/active_merchant/billing/gateways/vpos.rb index e5267a24615..25280eee8c3 100644 --- a/lib/active_merchant/billing/gateways/vpos.rb +++ b/lib/active_merchant/billing/gateways/vpos.rb @@ -9,7 +9,7 @@ class VposGateway < Gateway self.supported_countries = ['PY'] self.default_currency = 'PYG' - self.supported_cardtypes = %i[visa master] + self.supported_cardtypes = %i[visa master panal] self.homepage_url = 'https://comercios.bancard.com.py' self.display_name = 'vPOS' @@ -27,6 +27,7 @@ def initialize(options = {}) requires!(options, :private_key, :public_key) @private_key = options[:private_key] @public_key = options[:public_key] + @encryption_key = OpenSSL::PKey::RSA.new(options[:encryption_key]) if options[:encryption_key] @shop_process_id = options[:shop_process_id] || SecureRandom.random_number(10**15) super end @@ -114,15 +115,15 @@ def remove_invalid_utf_8_byte_sequences(transcript) transcript.encode('UTF-8', 'binary', undef: :replace, replace: '') end - private - # Required to encrypt PAN data. def one_time_public_key token = generate_token('get_encription_public_key', @public_key) response = commit(:pci_encryption_key, token: token) - OpenSSL::PKey::RSA.new(response.params['encryption_key']) + response.params['encryption_key'] end + private + def generate_token(*elements) Digest::MD5.hexdigest(@private_key + elements.join) end @@ -138,7 +139,9 @@ def add_card_data(post, payment) payload = { card_number: card_number, 'cvv': cvv }.to_json - post[:card_encrypted_data] = JWE.encrypt(payload, one_time_public_key) + encryption_key = @encryption_key || OpenSSL::PKey::RSA.new(one_time_public_key) + + post[:card_encrypted_data] = JWE.encrypt(payload, encryption_key) post[:card_month_expiration] = format(payment.month, :two_digits) post[:card_year_expiration] = format(payment.year, :two_digits) end @@ -155,10 +158,10 @@ def commit(action, parameters) url = build_request_url(action) begin response = parse(ssl_post(url, post_data(parameters))) - rescue ResponseError => response + rescue ResponseError => e # Errors are returned with helpful data, # but get filtered out by `ssl_post` because of their HTTP status. - response = parse(response.response.body) + response = parse(e.response.body) end Response.new( diff --git a/lib/active_merchant/billing/gateways/wirecard.rb b/lib/active_merchant/billing/gateways/wirecard.rb index 699e37177b4..60f1aa1eca8 100644 --- a/lib/active_merchant/billing/gateways/wirecard.rb +++ b/lib/active_merchant/billing/gateways/wirecard.rb @@ -179,11 +179,15 @@ def commit(action, money, options) message = response[:Message] authorization = response[:GuWID] - Response.new(success, message, response, + Response.new( + success, + message, + response, test: test?, authorization: authorization, avs_result: { code: avs_code(response, options) }, - cvv_result: response[:CVCResponseCode]) + cvv_result: response[:CVCResponseCode] + ) rescue ResponseError => e if e.response.code == '401' return Response.new(false, 'Invalid Login') @@ -405,7 +409,7 @@ def errors_to_string(root) 'N' => 'I', # CSC Match 'U' => 'U', # Data Not Checked 'Y' => 'D', # All Data Matched - 'Z' => 'P', # CSC and Postcode Matched + 'Z' => 'P' # CSC and Postcode Matched } # Amex have different AVS response codes to visa etc diff --git a/lib/active_merchant/billing/gateways/wompi.rb b/lib/active_merchant/billing/gateways/wompi.rb index 61112c08b79..ed0f4536039 100644 --- a/lib/active_merchant/billing/gateways/wompi.rb +++ b/lib/active_merchant/billing/gateways/wompi.rb @@ -61,12 +61,16 @@ def capture(money, authorization, options = {}) end def refund(money, authorization, options = {}) - post = { amount_in_cents: amount(money).to_i, transaction_id: authorization.to_s } - commit('refund', post, '/refunds_sync') + # post = { amount_in_cents: amount(money).to_i, transaction_id: authorization.to_s } + # commit('refund', post, '/refunds_sync') + + # All refunds will instead be voided. This is temporary. + void(authorization, options, money) end - def void(authorization, options = {}) - commit('void', {}, "/transactions/#{authorization}/void_sync") + def void(authorization, options = {}, money = nil) + post = money ? { amount_in_cents: amount(money).to_i } : {} + commit('void', post, "/transactions/#{authorization}/void_sync") end def supports_scrubbing? diff --git a/lib/active_merchant/billing/gateways/worldpay.rb b/lib/active_merchant/billing/gateways/worldpay.rb index 774f52a3d80..30e93ce882d 100644 --- a/lib/active_merchant/billing/gateways/worldpay.rb +++ b/lib/active_merchant/billing/gateways/worldpay.rb @@ -28,21 +28,6 @@ class WorldpayGateway < Gateway network_token: 'NETWORKTOKEN' } - CARD_CODES = { - 'visa' => 'VISA-SSL', - 'master' => 'ECMC-SSL', - 'discover' => 'DISCOVER-SSL', - 'american_express' => 'AMEX-SSL', - 'jcb' => 'JCB-SSL', - 'maestro' => 'MAESTRO-SSL', - 'diners_club' => 'DINERS-SSL', - 'elo' => 'ELO-SSL', - 'naranja' => 'NARANJA-SSL', - 'cabal' => 'CABAL-SSL', - 'unionpay' => 'CHINAUNIONPAY-SSL', - 'unknown' => 'CARD-SSL' - } - AVS_CODE_MAP = { 'A' => 'M', # Match 'B' => 'P', # Postcode matches, address not verified @@ -53,14 +38,14 @@ class WorldpayGateway < Gateway 'G' => 'C', # Address does not match, postcode not checked 'H' => 'I', # Address and postcode not provided 'I' => 'C', # Address not checked postcode does not match - 'J' => 'C', # Address and postcode does not match + 'J' => 'C' # Address and postcode does not match } CVC_CODE_MAP = { 'A' => 'M', # CVV matches 'B' => 'P', # Not provided 'C' => 'P', # Not checked - 'D' => 'N', # Does not match + 'D' => 'N' # Does not match } def initialize(options = {}) @@ -144,6 +129,11 @@ def store(credit_card, options = {}) store_request(credit_card, options) end + def inquire(authorization, options = {}) + order_id = order_id_from_authorization(authorization.to_s) || options[:order_id] + commit('direct_inquiry', build_order_inquiry_request(order_id, options), :ok, options) + end + def supports_scrubbing true end @@ -237,6 +227,7 @@ def build_authorization_request(money, payment_method, options) add_sub_merchant_data(xml, options[:sub_merchant_data]) if options[:sub_merchant_data] add_hcg_additional_data(xml, options) if options[:hcg_additional_data] add_instalments_data(xml, options) if options[:instalments] + add_additional_data(xml, money, options) if options[:level_2_data] || options[:level_3_data] add_moto_flag(xml, options) if options.dig(:metadata, :manual_entry) add_additional_3ds_data(xml, options) if options[:execute_threed] && options[:three_ds_version] && options[:three_ds_version] =~ /^2/ add_3ds_exemption(xml, options) if options[:exemption_type] @@ -245,6 +236,91 @@ def build_authorization_request(money, payment_method, options) end end + def add_additional_data(xml, amount, options) + level_two_data = options[:level_2_data] || {} + level_three_data = options[:level_3_data] || {} + level_two_and_three_data = level_two_data.merge(level_three_data).symbolize_keys + + xml.branchSpecificExtension do + xml.purchase do + add_level_two_and_three_data(xml, amount, level_two_and_three_data) + end + end + end + + def add_level_two_and_three_data(xml, amount, data) + xml.invoiceReferenceNumber data[:invoice_reference_number] if data.include?(:invoice_reference_number) + xml.customerReference data[:customer_reference] if data.include?(:customer_reference) + xml.cardAcceptorTaxId data[:card_acceptor_tax_id] if data.include?(:card_acceptor_tax_id) + + { + sales_tax: 'salesTax', + discount_amount: 'discountAmount', + shipping_amount: 'shippingAmount', + duty_amount: 'dutyAmount' + }.each do |key, tag| + next unless data.include?(key) + + xml.tag! tag do + data_amount = data[key].symbolize_keys + add_amount(xml, data_amount[:amount].to_i, data_amount) + end + end + + xml.discountName data[:discount_name] if data.include?(:discount_name) + xml.discountCode data[:discount_code] if data.include?(:discount_code) + + add_date_element(xml, 'shippingDate', data[:shipping_date]) if data.include?(:shipping_date) + + if data.include?(:shipping_courier) + xml.shippingCourier( + data[:shipping_courier][:priority], + data[:shipping_courier][:tracking_number], + data[:shipping_courier][:name] + ) + end + + add_optional_data_level_two_and_three(xml, data) + + if data.include?(:item) && data[:item].kind_of?(Array) + data[:item].each { |item| add_items_into_level_three_data(xml, item.symbolize_keys) } + elsif data.include?(:item) + add_items_into_level_three_data(xml, data[:item].symbolize_keys) + end + end + + def add_items_into_level_three_data(xml, item) + xml.item do + xml.description item[:description] if item[:description] + xml.productCode item[:product_code] if item[:product_code] + xml.commodityCode item[:commodity_code] if item[:commodity_code] + xml.quantity item[:quantity] if item[:quantity] + + { + unit_cost: 'unitCost', + item_total: 'itemTotal', + item_total_with_tax: 'itemTotalWithTax', + item_discount_amount: 'itemDiscountAmount', + tax_amount: 'taxAmount' + }.each do |key, tag| + next unless item.include?(key) + + xml.tag! tag do + data_amount = item[key].symbolize_keys + add_amount(xml, data_amount[:amount].to_i, data_amount) + end + end + end + end + + def add_optional_data_level_two_and_three(xml, data) + xml.shipFromPostalCode data[:ship_from_postal_code] if data.include?(:ship_from_postal_code) + xml.destinationPostalCode data[:destination_postal_code] if data.include?(:destination_postal_code) + xml.destinationCountryCode data[:destination_country_code] if data.include?(:destination_country_code) + add_date_element(xml, 'orderDate', data[:order_date].symbolize_keys) if data.include?(:order_date) + xml.taxExempt data[:tax_exempt] if data.include?(:tax_exempt) + end + def order_tag_attributes(options) { 'orderCode' => clean_order_id(options[:order_id]), 'installationId' => options[:inst_id] || @options[:inst_id] }.reject { |_, v| !v.present? } end @@ -555,7 +631,7 @@ def add_token_details(xml, options) end def add_card_details(xml, payment_method, options) - xml.tag! card_code_for(payment_method) do + xml.tag! 'CARD-SSL' do add_card(xml, payment_method, options) end end @@ -592,7 +668,8 @@ def add_card(xml, payment_method, options) 'year' => format(payment_method.year, :four_digits_year) ) end - xml.cardHolderName card_holder_name(payment_method, options) + name = card_holder_name(payment_method, options) + xml.cardHolderName name if name.present? xml.cvc payment_method.verification_value add_address(xml, (options[:billing_address] || options[:address]), options) @@ -718,9 +795,18 @@ def parse(action, xml) resp_params = { action: action } parse_elements(doc.root, resp_params) + extract_issuer_response(doc.root, resp_params) + resp_params end + def extract_issuer_response(doc, response) + return unless issuer_response = doc.at_xpath('//paymentService//reply//orderStatus//payment//IssuerResponseCode') + + response[:issuer_response_code] = issuer_response['code'] + response[:issuer_response_description] = issuer_response['description'] + end + def parse_elements(node, response) node_name = node.name.underscore node.attributes.each do |k, v| @@ -743,9 +829,12 @@ def headers(options) 'Content-Type' => 'text/xml', 'Authorization' => encoded_credentials } - if options[:cookie] - headers['Cookie'] = options[:cookie] if options[:cookie] - end + + # ensure cookie included on follow-up '3ds' and 'capture_request' calls, using the cookie saved from the preceding response + # cookie should be present in options on the 3ds and capture calls, but also still saved in the instance var in case + cookie = defined?(@cookie) ? @cookie : nil + cookie = options[:cookie] || cookie + headers['Cookie'] = cookie if cookie headers['Idempotency-Key'] = idempotency_key if idempotency_key headers @@ -761,7 +850,7 @@ def commit(action, request, *success_criteria, options) raw[:is3DSOrder] = true end success = success_from(action, raw, success_criteria) - message = message_from(success, raw, success_criteria) + message = message_from(success, raw, success_criteria, action) Response.new( success, @@ -798,7 +887,8 @@ def unparsable_response(raw_response) def handle_response(response) case response.code.to_i when 200...300 - @cookie = response['Set-Cookie'] + cookie = response.header['Set-Cookie']&.match('^[^;]*') + @cookie = cookie[0] if cookie response.body else raise ResponseError.new(response) @@ -809,10 +899,10 @@ def success_from(action, raw, success_criteria) success_criteria_success?(raw, success_criteria) || action_success?(action, raw) end - def message_from(success, raw, success_criteria) + def message_from(success, raw, success_criteria, action) return 'SUCCESS' if success - raw[:iso8583_return_code_description] || raw[:error] || required_status_message(raw, success_criteria) + raw[:iso8583_return_code_description] || raw[:error] || required_status_message(raw, success_criteria, action) || raw[:issuer_response_description] end # success_criteria can be: @@ -829,6 +919,8 @@ def action_success?(action, raw) case action when 'store' raw[:token].present? + when 'direct_inquiry' + raw[:last_event].present? else false end @@ -838,8 +930,11 @@ def error_code_from(success, raw) raw[:iso8583_return_code_code] || raw[:error_code] || nil unless success == 'SUCCESS' end - def required_status_message(raw, success_criteria) - "A transaction status of #{success_criteria.collect { |c| "'#{c}'" }.join(' or ')} is required." if !success_criteria.include?(raw[:last_event]) + def required_status_message(raw, success_criteria, action) + return if success_criteria.include?(raw[:last_event]) + return unless %w[cancel refund inquiry credit fast_credit].include?(action) + + "A transaction status of #{success_criteria.collect { |c| "'#{c}'" }.join(' or ')} is required." end def authorization_from(action, raw, options) @@ -887,13 +982,19 @@ def payment_details(payment_method) case payment_method when String token_type_and_details(payment_method) - when NetworkTokenizationCreditCard - { payment_type: :network_token } else - { payment_type: :credit } + type = network_token?(payment_method) ? :network_token : :credit + + { payment_type: type } end end + def network_token?(payment_method) + payment_method.respond_to?(:source) && + payment_method.respond_to?(:payment_cryptogram) && + payment_method.respond_to?(:eci) + end + def token_type_and_details(token) token_details = token_details_from_authorization(token) token_details[:payment_type] = token_details.has_key?(:token_id) ? :token : :pay_as_order @@ -919,10 +1020,6 @@ def currency_exponent(currency) return 2 end - def card_code_for(payment_method) - CARD_CODES[card_brand(payment_method)] || CARD_CODES['unknown'] - end - def eligible_for_0_auth?(payment_method, options = {}) payment_method.is_a?(CreditCard) && %w(visa master).include?(payment_method.brand) && options[:zero_dollar_auth] end diff --git a/lib/active_merchant/billing/gateways/worldpay_online_payments.rb b/lib/active_merchant/billing/gateways/worldpay_online_payments.rb index 2481dce0805..4ec743470d8 100644 --- a/lib/active_merchant/billing/gateways/worldpay_online_payments.rb +++ b/lib/active_merchant/billing/gateways/worldpay_online_payments.rb @@ -34,14 +34,16 @@ def capture(money, authorization, options = {}) if authorization commit(:post, "orders/#{CGI.escape(authorization)}/capture", { 'captureAmount' => money }, options, 'capture') else - Response.new(false, + Response.new( + false, 'FAILED', 'FAILED', test: test?, authorization: false, avs_result: {}, cvv_result: {}, - error_code: false) + error_code: false + ) end end @@ -170,14 +172,16 @@ def commit(method, url, parameters = nil, options = {}, type = false) authorization = response['message'] end - Response.new(success, + Response.new( + success, success ? 'SUCCESS' : response['message'], response, test: test?, authorization: authorization, avs_result: {}, cvv_result: {}, - error_code: success ? nil : response['customCode']) + error_code: success ? nil : response['customCode'] + ) end def test? diff --git a/lib/active_merchant/billing/gateways/xpay.rb b/lib/active_merchant/billing/gateways/xpay.rb new file mode 100644 index 00000000000..6aaefc99154 --- /dev/null +++ b/lib/active_merchant/billing/gateways/xpay.rb @@ -0,0 +1,135 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class XpayGateway < Gateway + self.display_name = 'XPay Gateway' + self.homepage_url = 'https://developer.nexi.it/en' + + self.test_url = 'https://stg-ta.nexigroup.com/api/phoenix-0.0/psp/api/v1/' + self.live_url = 'https://xpay.nexigroup.com/api/phoenix-0.0/psp/api/v1/' + + self.supported_countries = %w(AT BE CY EE FI FR DE GR IE IT LV LT LU MT PT SK SI ES BG HR DK NO PL RO RO SE CH HU) + self.default_currency = 'EUR' + self.currencies_without_fractions = %w(BGN HRK DKK NOK GBP PLN CZK RON SEK CHF HUF) + self.money_format = :cents + self.supported_cardtypes = %i[visa master maestro american_express jcb] + + ENDPOINTS_MAPPING = { + purchase: 'orders/2steps/payment', + authorize: 'orders/2steps/init', + capture: 'operations/{%s}/captures', + verify: 'orders/card_verification', + void: 'operations/{%s}/cancels', + refund: 'operations/{%s}/refunds' + } + + def initialize(options = {}) + requires!(options, :api_key) + @api_key = options[:api_key] + super + end + + def purchase(amount, payment_method, options = {}) + post = {} + commit('purchase', post, options) + end + + def authorize(amount, payment_method, options = {}) + post = {} + add_auth_purchase(post, amount, payment_method, options) + commit('authorize', post, options) + end + + def capture(amount, authorization, options = {}) + post = {} + commit('capture', post, options) + end + + def void(authorization, options = {}) + post = {} + commit('void', post, options) + end + + def refund(amount, authorization, options = {}) + post = {} + commit('refund', post, options) + end + + def verify(credit_card, options = {}) + post = {} + commit('verify', post, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) end + + private + + def add_invoice(post, money, options) end + + def add_payment_method(post, payment_method) end + + def add_reference(post, authorization) end + + def add_auth_purchase(post, money, payment, options) end + + def commit(action, params, options) + url = build_request_url(action) + response = ssl_post(url, params.to_json, request_headers(options)) + + unless response + Response.new( + success_from(response), + message_from(success_from(response), response), + response, + authorization: authorization_from(response), + avs_result: AVSResult.new(code: response['some_avs_result_key']), + cvv_result: CVVResult.new(response['some_cvv_result_key']), + test: test?, + error_code: error_code_from(response) + ) + end + rescue ResponseError => e + response = e.response.body + JSON.parse(response) + end + + def request_headers(options) + headers = { + 'Content-Type' => 'application/json', + 'X-Api-Key' => @api_key, + 'Correlation-Id' => options.dig(:order, :order_id) || SecureRandom.uuid + } + headers + end + + def build_request_url(action, id = nil) + base_url = test? ? test_url : live_url + endpoint = ENDPOINTS_MAPPING[action.to_sym] % id + base_url + endpoint + end + + def success_from(response) + response == 'SUCCESS' + end + + def message_from(succeeded, response) + if succeeded + 'Succeeded' + else + response.dig('errors') unless response + end + end + + def authorization_from(response) + response.dig('latest_payment_attempt', 'payment_intent_id') unless response + end + + def error_code_from(response) + response['provider_original_response_code'] || response['code'] unless success_from(response) + end + end + end +end diff --git a/lib/active_merchant/billing/response.rb b/lib/active_merchant/billing/response.rb index 1d4b7fd5ceb..fb1c502d7ee 100644 --- a/lib/active_merchant/billing/response.rb +++ b/lib/active_merchant/billing/response.rb @@ -85,7 +85,21 @@ def success? (primary_response ? primary_response.success? : true) end - %w(params message test authorization avs_result cvv_result error_code emv_authorization test? fraud_review?).each do |m| + def avs_result + return @primary_response.try(:avs_result) if @use_first_response + + result = responses.reverse.find { |r| r.avs_result['code'].present? } + result.try(:avs_result) || responses.last.try(:avs_result) + end + + def cvv_result + return @primary_response.try(:cvv_result) if @use_first_response + + result = responses.reverse.find { |r| r.cvv_result['code'].present? } + result.try(:cvv_result) || responses.last.try(:cvv_result) + end + + %w(params message test authorization error_code emv_authorization test? fraud_review?).each do |m| class_eval %( def #{m} (@responses.empty? ? nil : primary_response.#{m}) diff --git a/lib/active_merchant/connection.rb b/lib/active_merchant/connection.rb index c15ac5bb803..626881a136e 100644 --- a/lib/active_merchant/connection.rb +++ b/lib/active_merchant/connection.rb @@ -85,8 +85,6 @@ def request(method, body, headers = {}) result = case method when :get - raise ArgumentError, 'GET requests do not support a request body' if body - http.get(endpoint.request_uri, headers) when :post debug body diff --git a/lib/active_merchant/country.rb b/lib/active_merchant/country.rb index 3070ab65ad3..6fee9d6a874 100644 --- a/lib/active_merchant/country.rb +++ b/lib/active_merchant/country.rb @@ -184,6 +184,7 @@ def to_s { alpha2: 'KP', name: 'Korea, Democratic People\'s Republic of', alpha3: 'PRK', numeric: '408' }, { alpha2: 'KR', name: 'Korea, Republic of', alpha3: 'KOR', numeric: '410' }, { alpha2: 'XK', name: 'Kosovo', alpha3: 'XKX', numeric: '900' }, + { alpha2: 'QZ', name: 'Kosovo', alpha3: 'XKX', numeric: '900' }, { alpha2: 'KW', name: 'Kuwait', alpha3: 'KWT', numeric: '414' }, { alpha2: 'KG', name: 'Kyrgyzstan', alpha3: 'KGZ', numeric: '417' }, { alpha2: 'LA', name: 'Lao People\'s Democratic Republic', alpha3: 'LAO', numeric: '418' }, diff --git a/lib/active_merchant/errors.rb b/lib/active_merchant/errors.rb index af4bcb8b1be..b017c45114a 100644 --- a/lib/active_merchant/errors.rb +++ b/lib/active_merchant/errors.rb @@ -23,10 +23,13 @@ def initialize(response, message = nil) end def to_s - "Failed with #{response.code} #{response.message if response.respond_to?(:message)}" + "Failed with #{response.code if response.respond_to?(:code)} #{response.message if response.respond_to?(:message)}" end end + class OAuthResponseError < ResponseError # :nodoc: + end + class ClientCertificateError < ActiveMerchantError # :nodoc end diff --git a/lib/active_merchant/version.rb b/lib/active_merchant/version.rb index 6d05212afb0..68017e31781 100644 --- a/lib/active_merchant/version.rb +++ b/lib/active_merchant/version.rb @@ -1,3 +1,3 @@ module ActiveMerchant - VERSION = '1.126.0' + VERSION = '1.135.0' end diff --git a/lib/support/ssl_verify.rb b/lib/support/ssl_verify.rb index 9f431a8da48..eb5a9c61157 100644 --- a/lib/support/ssl_verify.rb +++ b/lib/support/ssl_verify.rb @@ -80,9 +80,9 @@ def ssl_verify_peer?(uri) end return :success - rescue OpenSSL::SSL::SSLError => ex - return :fail, ex.inspect - rescue Net::HTTPBadResponse, Errno::ETIMEDOUT, EOFError, SocketError, Errno::ECONNREFUSED, Timeout::Error => ex - return :error, ex.inspect + rescue OpenSSL::SSL::SSLError => e + return :fail, e.inspect + rescue Net::HTTPBadResponse, Errno::ETIMEDOUT, EOFError, SocketError, Errno::ECONNREFUSED, Timeout::Error => e + return :error, e.inspect end end diff --git a/lib/support/ssl_version.rb b/lib/support/ssl_version.rb index 3050d09f438..7a6def3a5d5 100644 --- a/lib/support/ssl_version.rb +++ b/lib/support/ssl_version.rb @@ -75,12 +75,12 @@ def test_min_version(uri, min_version) return :success rescue Net::HTTPBadResponse return :success # version negotiation succeeded - rescue OpenSSL::SSL::SSLError => ex - return :fail, ex.inspect - rescue Interrupt => ex + rescue OpenSSL::SSL::SSLError => e + return :fail, e.inspect + rescue Interrupt => e print_summary - raise ex - rescue StandardError => ex - return :error, ex.inspect + raise e + rescue StandardError => e + return :error, e.inspect end end diff --git a/test/fixtures.yml b/test/fixtures.yml index 86d6b94e649..43c848a643a 100644 --- a/test/fixtures.yml +++ b/test/fixtures.yml @@ -20,6 +20,10 @@ airwallex: client_id: SOMECREDENTIAL client_api_key: ANOTHERCREDENTIAL +alelo: + client_id: xxxxxxx + client_secret: xxxxxxx + allied_wallet: site_id: site_id merchant_id: merchant_id @@ -117,6 +121,7 @@ braintree_blue_with_ach_enabled: public_key: Y private_key: Z merchant_account_id: A + venmo_profile_id: B braintree_blue_with_processing_rules: merchant_id: X @@ -189,7 +194,13 @@ checkout: password: Password1! checkout_v2: - secret_key: secret_key + secret_key: SECRET_KEY_FOR_BASIC_TRANSACTIONS + client_id: CLIENT_ID_FOR_OAUTH_TRANSACTIONS + client_secret: CLIENT_SECRET_FOR_OAUTH_TRANSACTIONS + +checkout_v2_token: + secret_key: sk_sbox_xxxxxxxxxxxxxxxxx + public_key: pk_sbox_xxxxxxxxxxxxxxxxx citrus_pay: userid: CPF00001 @@ -213,6 +224,11 @@ clearhaus_secure: InviQqJd1KTGRDmWIGrE5YACVmW2JSszD9t5VKxkAA== -----END RSA PRIVATE KEY----- +commerce_hub: + api_key: API KEY + api_secret: API SECRET + merchant_id: MERCHANT ID + terminal_id: TERMINAL ID # Contact Support at it_support@commercegate.com for credentials and offer/site commercegate: @@ -260,6 +276,12 @@ cyber_source_latam_pe: login: merchant_id password: soap_key +# Working credentials, no need to replace +cybersource_rest: + merchant_id: "testrest" + public_key: "08c94330-f618-42a3-b09d-e1e43be5efda" + private_key: "yBJxy6LjM2TmcPGu+GaJrHtkke25fPpUX+UY6/L/1tE=" + # Working credentials, no need to replace d_local: login: aeaf9bbfa1 @@ -286,6 +308,11 @@ decidir_plus_preauth: decidir_purchase: api_key: 5df6b5764c3f4822aecdc82d56f26b9d +deepstack: + publishable_api_key: pk_test_7H5GkZJ4ktV38eZxKDItVMZZvluUhORE + app_id: sk_test_8fe27907-c359-4fe4-ad9b-eaaa + shared_secret: JC6zgUX3oZ9vRshFsM98lXzH4tu6j4ZfB4cSOqOX/xQ= + # No working test credentials dibs: merchant_id: SOMECREDENTIAL @@ -417,8 +444,13 @@ garanti: global_collect: merchant_id: 2196 - api_key_id: c91d6752cbbf9cf1 - secret_api_key: xHjQr5gL9Wcihkqoj4w/UQugdSCNXM2oUQHG5C82jy4= + api_key_id: b2311c2c832dd238 + secret_api_key: Av5wKihoVlLN8SnGm6669hBHyG4Y4aS4KwaZUCvEIbY= + +global_collect_direct: + merchant_id: "NamastayTest" + api_key_id: "CF4CDF3F45F13C5CCBD0" + secret_api_key: "mvcEXR7Rem+KJE/atKsQ3Luqv37VEvTe2VOH5/Ibqd90VDzQ71Ht41RBVVyJuebzGnFu30dYpptgdrCcNvAu5A==" global_transport: global_user_name: "USERNAME" @@ -875,12 +907,17 @@ payway_dot_com: pin: api_key: "I_mo9BUUUXIwXF-avcs3LA" +plexo: + client_id: YOUR_CLIENT_ID + api_key: YOUR_API_KEY + merchant_id: YOUR_MERCHANT_ID + plugnpay: login: LOGIN password: PASSWORD priority: - key: SANDBOX_KEY + api_key: SANDBOX_KEY secret: SECRET merchant_id: MERCHANT_ID @@ -1108,6 +1145,10 @@ raven_pac_net: secret: "all good men die young" prn: 987743 +reach: + merchant_id: 'xxxxxxx' + secret: 'xxxxxxx' + realex: login: X password: Y @@ -1246,11 +1287,17 @@ secure_pay_tech: securion_pay: secret_key: pr_test_qZN4VVIKCySfCeXCBoHO9DBe +shift4: + client_guid: YOUR_CLIENT_ID + auth_token: YOUR_AUTH_TOKEN + +shift4_v2: + secret_key: pr_test_xxxxxxxxx + # Working credentials, no need to replace simetrik: client_id: 'wNhJBdrKDk3vTmkQMAWi5zWN7y21adO3' client_secret: 'fq2riPpiDJaAwS4_UMAXZy1_nU1jNGz0F6gAFWOJFNmm_TfC8EFiHwMmGKAEDkwY' - audience: 'https://tenant-payments-dev.us.auth0.com/api/v2/' # Replace with your serial numbers for the skipjack test environment skipjack: @@ -1267,10 +1314,6 @@ spreedly_core: password: "Y2i7AjgU03SUjwY4xnOPqzdsv4dMbPDCQzorAk8Bcoy0U8EIVE4innGjuoMQv7MN" gateway_token: "3gLeg4726V5P0HK7cq7QzHsL0a6" -# Working credentials, no need to replace -square: - access_token: EAAAEBfybUCoyfELbbSshWYKna9FYluyA56pcgXDNtSDULMWEah5Ci4S8XcPKBYz - # Working credentials, no need to replace stripe: login: sk_test_3OD4TdKSIOhDOL2146JJcC79 @@ -1284,6 +1327,10 @@ stripe_verified_bank_account: customer_id: "cus_7s22nNueP2Hjj6" bank_account_id: "ba_17cHxeAWOtgoysogv3NM8CJ1" +sum_up: + access_token: SOMECREDENTIAL + pay_to_email: SOMECREDENTIAL + # Working credentials, no need to replace swipe_checkout: login: 2077103073D8B5 @@ -1381,6 +1428,7 @@ visanet_peru: vpos: public_key: SOMECREDENTIAL private_key: ANOTHERCREDENTIAL + encryption_key: "-----BEGIN PUBLIC KEY-----\n ..." commerce: 123 commerce_branch: 45 @@ -1430,3 +1478,6 @@ worldpay_us: acctid: MPNAB subid: SPREE merchantpin: "1234567890" + +x_pay: + api_key: 2d708950-50a1-434e-9a93-5d3ae2f1dd9f diff --git a/test/remote/gateways/remote_adyen_test.rb b/test/remote/gateways/remote_adyen_test.rb index 572bf39b3a7..dfa28c6ded1 100644 --- a/test/remote/gateways/remote_adyen_test.rb +++ b/test/remote/gateways/remote_adyen_test.rb @@ -12,67 +12,83 @@ def setup @general_bank_account = check(name: 'A. Klaassen', account_number: '123456789', routing_number: 'NL13TEST0123456789') - @credit_card = credit_card('4111111111111111', + @credit_card = credit_card( + '4111111111111111', month: 3, year: 2030, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'visa') + brand: 'visa' + ) - @avs_credit_card = credit_card('4400000000000008', + @avs_credit_card = credit_card( + '4400000000000008', month: 3, year: 2030, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'visa') + brand: 'visa' + ) - @elo_credit_card = credit_card('5066 9911 1111 1118', + @elo_credit_card = credit_card( + '5066 9911 1111 1118', month: 3, year: 2030, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'elo') + brand: 'elo' + ) - @three_ds_enrolled_card = credit_card('4917610000000000', + @three_ds_enrolled_card = credit_card( + '4917610000000000', month: 3, year: 2030, verification_value: '737', - brand: :visa) + brand: :visa + ) - @cabal_credit_card = credit_card('6035 2277 1642 7021', + @cabal_credit_card = credit_card( + '6035 2277 1642 7021', month: 3, year: 2030, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'cabal') + brand: 'cabal' + ) - @invalid_cabal_credit_card = credit_card('6035 2200 0000 0006', + @invalid_cabal_credit_card = credit_card( + '6035 2200 0000 0006', month: 3, year: 2030, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'cabal') + brand: 'cabal' + ) - @unionpay_credit_card = credit_card('8171 9999 0000 0000 021', + @unionpay_credit_card = credit_card( + '8171 9999 0000 0000 021', month: 10, year: 2030, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'unionpay') + brand: 'unionpay' + ) - @invalid_unionpay_credit_card = credit_card('8171 9999 1234 0000 921', + @invalid_unionpay_credit_card = credit_card( + '8171 9999 1234 0000 921', month: 10, year: 2030, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'unionpay') + brand: 'unionpay' + ) @declined_card = credit_card('4000300011112220') @@ -112,13 +128,33 @@ def setup verification_value: '737', brand: 'visa' ) + @us_address = { + address1: 'Brannan Street', + address2: '121', + country: 'US', + city: 'Beverly Hills', + state: 'CA', + zip: '90210' + } + + @payout_options = { + reference: 'P9999999999999999', + email: 'john.smith@test.com', + ip: '77.110.174.153', + shopper_reference: 'John Smith', + billing_address: @us_address, + nationality: 'NL', + order_id: 'P9999999999999999', + date_of_birth: '1990-01-01', + payout: true + } @options = { reference: '345123', email: 'john.smith@test.com', ip: '77.110.174.153', shopper_reference: 'John Smith', - billing_address: address(country: 'US', state: 'CA'), + billing_address: @us_address, order_id: '123', stored_credential: { reason_type: 'unscheduled' } } @@ -136,7 +172,7 @@ def setup notification_url: 'https://example.com/notification', browser_info: { accept_header: 'unknown', - depth: 100, + depth: 48, java: false, language: 'US', height: 1000, @@ -148,44 +184,6 @@ def setup } @long_order_id = 'asdfjkl;asdfjkl;asdfj;aiwyutinvpoaieryutnmv;203987528752098375j3q-p489756ijmfpvbijpq348nmdf;vbjp3845' - - @sub_seller_options = { - "subMerchant.numberOfSubSellers": '2', - "subMerchant.subSeller1.id": '111111111', - "subMerchant.subSeller1.name": 'testSub1', - "subMerchant.subSeller1.street": 'Street1', - "subMerchant.subSeller1.postalCode": '12242840', - "subMerchant.subSeller1.city": 'Sao jose dos campos', - "subMerchant.subSeller1.state": 'SP', - "subMerchant.subSeller1.country": 'BRA', - "subMerchant.subSeller1.taxId": '12312312340', - "subMerchant.subSeller1.mcc": '5691', - "subMerchant.subSeller1.debitSettlementBank": '1', - "subMerchant.subSeller1.debitSettlementAgency": '1', - "subMerchant.subSeller1.debitSettlementAccountType": '1', - "subMerchant.subSeller1.debitSettlementAccount": '1', - "subMerchant.subSeller1.creditSettlementBank": '1', - "subMerchant.subSeller1.creditSettlementAgency": '1', - "subMerchant.subSeller1.creditSettlementAccountType": '1', - "subMerchant.subSeller1.creditSettlementAccount": '1', - "subMerchant.subSeller2.id": '22222222', - "subMerchant.subSeller2.name": 'testSub2', - "subMerchant.subSeller2.street": 'Street2', - "subMerchant.subSeller2.postalCode": '12300000', - "subMerchant.subSeller2.city": 'Jacarei', - "subMerchant.subSeller2.state": 'SP', - "subMerchant.subSeller2.country": 'BRA', - "subMerchant.subSeller2.taxId": '12312312340', - "subMerchant.subSeller2.mcc": '5691', - "subMerchant.subSeller2.debitSettlementBank": '1', - "subMerchant.subSeller2.debitSettlementAgency": '1', - "subMerchant.subSeller2.debitSettlementAccountType": '1', - "subMerchant.subSeller2.debitSettlementAccount": '1', - "subMerchant.subSeller2.creditSettlementBank": '1', - "subMerchant.subSeller2.creditSettlementAgency": '1', - "subMerchant.subSeller2.creditSettlementAccountType": '1', - "subMerchant.subSeller2.creditSettlementAccount": '1' - } end def test_successful_authorize @@ -343,13 +341,15 @@ def test_successful_authorize_with_3ds2_app_based_request # with rule set in merchant account to skip 3DS for cards of this brand def test_successful_authorize_with_3ds_dynamic_rule_broken - mastercard_threed = credit_card('5212345678901234', + mastercard_threed = credit_card( + '5212345678901234', month: 3, year: 2030, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'mastercard') + brand: 'mastercard' + ) assert response = @gateway.authorize(@amount, mastercard_threed, @options.merge(threed_dynamic: true)) assert response.test? refute response.authorization.blank? @@ -363,7 +363,7 @@ def test_successful_authorize_with_3ds_dynamic_rule_broken def test_purchase_fails_on_unexpected_3ds_initiation response = @gateway.purchase(8484, @three_ds_enrolled_card, @options) assert_failure response - assert_match 'Received unexpected 3DS authentication response', response.message + assert_match 'Received unexpected 3DS authentication response, but a 3DS initiation flag was not included in the request.', response.message end def test_successful_purchase_with_auth_data_via_threeds1_standalone @@ -464,6 +464,12 @@ def test_failed_authorize_with_bank_account assert_equal 'Bank Account or Bank Location Id not valid or missing', response.message end + def test_failed_authorize_with_bank_account_missing_country_code + response = @gateway.authorize(@amount, @bank_account, @options.except(:billing_address)) + assert_failure response + assert_equal 'BankDetails missing', response.message + end + def test_successful_purchase response = @gateway.purchase(@amount, @credit_card, @options) assert_success response @@ -769,6 +775,36 @@ def test_failed_credit assert_equal "Required field 'reference' is not provided.", response.message end + def test_successful_payout_with_credit_card + response = @gateway.credit(2500, @credit_card, @payout_options) + + assert_success response + assert_equal 'Authorised', response.message + end + + def test_successful_payout_with_fund_source + fund_source = { + fund_source: { + additional_data: { fundingSource: 'Debit' }, + first_name: 'Payer', + last_name: 'Name', + billing_address: @us_address + } + } + + response = @gateway.credit(2500, @credit_card, @payout_options.merge!(fund_source)) + + assert_success response + assert_equal 'Authorised', response.message + end + + def test_failed_payout_with_credit_card + response = @gateway.credit(2500, @credit_card, @payout_options.except(:date_of_birth)) + + assert_failure response + assert_equal 'Payout has been refused due to regulatory reasons', response.message + end + def test_successful_void auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -993,6 +1029,14 @@ def test_successful_tokenize_only_store assert_equal 'Success', response.message end + def test_successful_tokenize_only_store_with_ntid + assert response = @gateway.store(@credit_card, @options.merge({ tokenize_only: true, network_transaction_id: '858435661128555' })) + + assert_success response + assert !response.authorization.split('#')[2].nil? + assert_equal 'Success', response.message + end + def test_successful_store_with_elo_card assert response = @gateway.store(@elo_credit_card, @options) @@ -1001,11 +1045,9 @@ def test_successful_store_with_elo_card assert_equal 'Authorised', response.message end - # Adyen does not currently support recurring transactions with Cabal cards - def test_failed_store_with_cabal_card + def test_successful_store_with_cabal_card assert response = @gateway.store(@cabal_credit_card, @options) - assert_failure response - assert_equal 'Recurring transactions are not supported for this card type.', response.message + assert_success response end def test_successful_store_with_unionpay_card @@ -1147,7 +1189,7 @@ def test_invalid_expiry_month_for_purchase card = credit_card('4242424242424242', month: 16) assert response = @gateway.purchase(@amount, card, @options) assert_failure response - assert_equal 'The provided Expiry Date is not valid.: Expiry month should be between 1 and 12 inclusive', response.message + assert_equal 'The provided Expiry Date is not valid.: Expiry month should be between 1 and 12 inclusive: 16', response.message end def test_invalid_expiry_year_for_purchase @@ -1329,6 +1371,47 @@ def test_auth_capture_refund_with_network_txn_id assert_success refund end + def test_successful_capture_with_shopper_statement + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization, @options.merge(shopper_statement: 'test1234')) + assert_success capture + end + + def test_purchase_with_skip_mpi_data + options = { + reference: '345123', + email: 'john.smith@test.com', + ip: '77.110.174.153', + shopper_reference: 'shopper 123', + billing_address: address(country: 'US', state: 'CA') + } + first_options = options.merge( + order_id: generate_unique_id, + shopper_interaction: 'Ecommerce', + recurring_processing_model: 'Subscription' + ) + assert auth = @gateway.authorize(@amount, @apple_pay_card, first_options) + assert_success auth + + assert_equal 'Subscription', auth.params['additionalData']['recurringProcessingModel'] + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal '[capture-received]', capture.message + + used_options = options.merge( + order_id: generate_unique_id, + skip_mpi_data: 'Y', + shopper_interaction: 'ContAuth', + recurring_processing_model: 'Subscription', + network_transaction_id: auth.network_transaction_id + ) + + assert purchase = @gateway.purchase(@amount, @apple_pay_card, used_options) + assert_success purchase + end + def test_successful_authorize_with_sub_merchant_data sub_merchant_data = { sub_merchant_id: '123451234512345', @@ -1359,12 +1442,185 @@ def test_successful_authorize_with_sub_merchant_data end def test_successful_authorize_with_sub_merchant_sub_seller_data + @sub_seller_options = { + "subMerchant.numberOfSubSellers": '2', + "subMerchant.subSeller1.id": '111111111', + "subMerchant.subSeller1.name": 'testSub1', + "subMerchant.subSeller1.street": 'Street1', + "subMerchant.subSeller1.postalCode": '12242840', + "subMerchant.subSeller1.city": 'Sao jose dos campos', + "subMerchant.subSeller1.state": 'SP', + "subMerchant.subSeller1.country": 'BRA', + "subMerchant.subSeller1.taxId": '12312312340', + "subMerchant.subSeller1.mcc": '5691', + "subMerchant.subSeller1.debitSettlementBank": '1', + "subMerchant.subSeller1.debitSettlementAgency": '1', + "subMerchant.subSeller1.debitSettlementAccountType": '1', + "subMerchant.subSeller1.debitSettlementAccount": '1', + "subMerchant.subSeller1.creditSettlementBank": '1', + "subMerchant.subSeller1.creditSettlementAgency": '1', + "subMerchant.subSeller1.creditSettlementAccountType": '1', + "subMerchant.subSeller1.creditSettlementAccount": '1', + "subMerchant.subSeller2.id": '22222222', + "subMerchant.subSeller2.name": 'testSub2', + "subMerchant.subSeller2.street": 'Street2', + "subMerchant.subSeller2.postalCode": '12300000', + "subMerchant.subSeller2.city": 'Jacarei', + "subMerchant.subSeller2.state": 'SP', + "subMerchant.subSeller2.country": 'BRA', + "subMerchant.subSeller2.taxId": '12312312340', + "subMerchant.subSeller2.mcc": '5691', + "subMerchant.subSeller2.debitSettlementBank": '1', + "subMerchant.subSeller2.debitSettlementAgency": '1', + "subMerchant.subSeller2.debitSettlementAccountType": '1', + "subMerchant.subSeller2.debitSettlementAccount": '1', + "subMerchant.subSeller2.creditSettlementBank": '1', + "subMerchant.subSeller2.creditSettlementAgency": '1', + "subMerchant.subSeller2.creditSettlementAccountType": '1', + "subMerchant.subSeller2.creditSettlementAccount": '1' + } assert response = @gateway.authorize(@amount, @avs_credit_card, @options.merge(sub_merchant_data: @sub_seller_options)) assert response.test? refute response.authorization.blank? assert_success response end + def test_successful_authorize_with_level_2_data + level_2_data = { + total_tax_amount: '160', + customer_reference: '101' + } + assert response = @gateway.authorize(@amount, @avs_credit_card, @options.merge(level_2_data: level_2_data)) + assert response.test? + refute response.authorization.blank? + assert_success response + end + + def test_successful_purchase_with_level_2_data + level_2_data = { + total_tax_amount: '160', + customer_reference: '101' + } + response = @gateway.purchase(@amount, @credit_card, @options.merge(level_2_data: level_2_data)) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_authorize_with_level_3_data + level_3_data = { + total_tax_amount: '12800', + customer_reference: '101', + freight_amount: '300', + destination_state_province_code: 'NYC', + ship_from_postal_code: '1082GM', + order_date: '101216', + destination_postal_code: '1082GM', + destination_country_code: 'NLD', + duty_amount: '500', + items: [ + { + description: 'T16 Test products 1', + product_code: 'TEST120', + commodity_code: 'COMMCODE1', + quantity: '5', + unit_of_measure: 'm', + unit_price: '1000', + discount_amount: '60', + total_amount: '4940' + } + ] + } + assert response = @gateway.authorize(@amount, @avs_credit_card, @options.merge(level_3_data: level_3_data)) + assert response.test? + assert_success response + end + + def test_successful_purchase_with_level_3_data + level_3_data = { + total_tax_amount: '12800', + customer_reference: '101', + freight_amount: '300', + destination_state_province_code: 'NYC', + ship_from_postal_code: '1082GM', + order_date: '101216', + destination_postal_code: '1082GM', + destination_country_code: 'NLD', + duty_amount: '500', + items: [ + { + description: 'T16 Test products 1', + product_code: 'TEST120', + commodity_code: 'COMMCODE1', + quantity: '5', + unit_of_measure: 'm', + unit_price: '1000', + discount_amount: '60', + total_amount: '4940' + } + ] + } + response = @gateway.purchase(@amount, @credit_card, @options.merge(level_3_data: level_3_data)) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_succesful_purchase_with_airline_data + airline_data = { + agency_invoice_number: 'BAC123', + agency_plan_name: 'plan name', + airline_code: '434234', + airline_designator_code: '1234', + boarding_fee: '100', + computerized_reservation_system: 'abcd', + customer_reference_number: 'asdf1234', + document_type: 'cc', + flight_date: '2023-09-08', + ticket_issue_address: 'abcqwer', + ticket_number: 'ABCASDF', + travel_agency_code: 'ASDF', + travel_agency_name: 'hopper', + passenger_name: 'Joe Doe', + leg: { + carrier_code: 'KL', + class_of_travel: 'F' + }, + passenger: { + first_name: 'Joe', + last_name: 'Doe', + telephone_number: '432211111' + } + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(additional_data_airline: airline_data)) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_succesful_purchase_with_lodging_data + lodging_data = { + check_in_date: '20230822', + check_out_date: '20230830', + customer_service_toll_free_number: '234234', + fire_safety_act_indicator: 'abc123', + folio_cash_advances: '1234667', + folio_number: '32343', + food_beverage_charges: '1234', + no_show_indicator: 'Y', + prepaid_expenses: '100', + property_phone_number: '54545454', + number_of_nights: '5', + rate: '100', + total_room_tax: '1000', + total_tax: '100', + duration: '2', + market: 'H' + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(additional_data_lodging: lodging_data)) + assert_success response + assert_equal '[capture-received]', response.message + end + def test_successful_cancel_or_refund auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -1408,6 +1664,31 @@ def test_successful_cancel_or_refund_passing_capture assert_void_references_original_authorization(void, auth) end + def test_successful_authorize_with_alternate_kosovo_code + @options[:billing_address][:country] = 'XK' + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal 'Billing address problem (Country XK invalid)', response.message + + @options[:billing_address][:country] = 'QZ' + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + end + + def test_successful_authorize_with_address_override + address = { + address1: 'Bag End', + address2: '123 Hobbiton Way', + city: 'Hobbiton', + state: 'Derbyshire', + country: 'GB', + zip: 'DE45 1PP' + } + response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: address, address_override: true)) + assert_success response + assert_equal '[capture-received]', response.message + end + private def stored_credential_options(*args, ntid: nil) diff --git a/test/remote/gateways/remote_airwallex_test.rb b/test/remote/gateways/remote_airwallex_test.rb index ce4146754ad..5cbc4053c7d 100644 --- a/test/remote/gateways/remote_airwallex_test.rb +++ b/test/remote/gateways/remote_airwallex_test.rb @@ -33,14 +33,20 @@ def test_successful_purchase_with_address end def test_successful_purchase_with_specified_ids - request_id = "request_#{(Time.now.to_f.round(2) * 100).to_i}" - merchant_order_id = "order_#{(Time.now.to_f.round(2) * 100).to_i}" + request_id = SecureRandom.uuid + merchant_order_id = SecureRandom.uuid response = @gateway.purchase(@amount, @credit_card, @options.merge(request_id: request_id, merchant_order_id: merchant_order_id)) assert_success response assert_match(request_id, response.params.dig('request_id')) assert_match(merchant_order_id, response.params.dig('merchant_order_id')) end + def test_successful_purchase_with_skip_3ds + response = @gateway.purchase(@amount, @credit_card, @options.merge({ skip_3ds: 'true' })) + assert_success response + assert_equal 'AUTHORIZED', response.message + end + def test_failed_purchase response = @gateway.purchase(@declined_amount, @declined_card, @options) assert_failure response @@ -84,10 +90,10 @@ def test_failed_capture end def test_successful_refund - purchase = @gateway.purchase(@amount, @credit_card, @options.merge(generated_ids)) + purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount, purchase.authorization, @options.merge(generated_ids)) + assert refund = @gateway.refund(@amount, purchase.authorization, @options) assert_success refund assert_equal 'RECEIVED', refund.message end @@ -107,7 +113,7 @@ def test_failed_refund end def test_successful_void - auth = @gateway.authorize(@amount, @credit_card, @options.merge(generated_ids)) + auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth assert void = @gateway.void(auth.authorization, @options) @@ -241,11 +247,6 @@ def test_successful_purchase_with_3ds_v2_options private - def generated_ids - timestamp = (Time.now.to_f.round(2) * 100).to_i.to_s - { request_id: timestamp.to_s, merchant_order_id: "mid_#{timestamp}" } - end - def add_cit_network_transaction_id_to_stored_credential(auth) @stored_credential_mit_options[:network_transaction_id] = auth.params['latest_payment_attempt']['provider_transaction_id'] end diff --git a/test/remote/gateways/remote_alelo_test.rb b/test/remote/gateways/remote_alelo_test.rb new file mode 100644 index 00000000000..be4d9ae9059 --- /dev/null +++ b/test/remote/gateways/remote_alelo_test.rb @@ -0,0 +1,202 @@ +require 'test_helper' +require 'singleton' + +class RemoteAleloTest < Test::Unit::TestCase + def setup + @gateway = AleloGateway.new(fixtures(:alelo)) + + @amount = 100 + @credit_card = credit_card('4000100011112224') + @declined_card = credit_card('4000300011112220') + @options = { + order_id: '1', + establishment_code: '000002007690360', + sub_merchant_mcc: '5499', + player_identification: '1', + description: 'Store Purchase', + external_trace_number: '123456' + } + end + + def test_access_token_success + resp = @gateway.send :fetch_access_token + + assert_kind_of Response, resp + refute_nil resp.message + end + + def test_failure_access_token_with_invalid_keys + error = assert_raises(ActiveMerchant::ResponseError) do + gateway = AleloGateway.new({ client_id: 'abc123', client_secret: 'abc456' }) + gateway.send :fetch_access_token + end + + assert_match(/401/, error.message) + end + + def test_successful_remote_encryption_key_with_provided_access_token + access_token = @gateway.send :fetch_access_token + resp = @gateway.send(:remote_encryption_key, access_token.message) + + assert_kind_of Response, resp + refute_nil resp.message + end + + def test_ensure_credentials_with_no_provided_access_token_key_are_generated + credentials = @gateway.send :ensure_credentials, {} + + refute_nil credentials[:key] + refute_nil credentials[:access_token] + assert_kind_of Response, credentials[:multiresp] + assert_equal 2, credentials[:multiresp].responses.size + end + + def test_sucessful_encryption_key_requested_when_access_token_provided + access_token = @gateway.send :fetch_access_token + @gateway.options[:access_token] = access_token.message + credentials = @gateway.send :ensure_credentials + + refute_nil credentials[:key] + refute_nil credentials[:access_token] + assert_equal access_token.message, credentials[:access_token] + assert_kind_of Response, credentials[:multiresp] + assert_equal 1, credentials[:multiresp].responses.size + end + + def test_successful_fallback_with_expired_access_token + @gateway.options[:access_token] = 'abc123' + credentials = @gateway.send :ensure_credentials + + refute_nil credentials[:key] + refute_nil credentials[:access_token] + refute_equal 'abc123', credentials[:access_token] + assert_kind_of Response, credentials[:multiresp] + assert_equal 2, credentials[:multiresp].responses.size + end + + def test_successful_purchase + set_credentials! + @gateway.options[:encryption_uuid] = '53141521-afc8-4a08-af0c-f0382aef43c1' + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_match %r{confirmada}i, response.message + end + + def test_successful_purchase_with_no_predefined_credentials + @gateway.options[:encryption_uuid] = '53141521-afc8-4a08-af0c-f0382aef43c1' + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_match %r{confirmada}i, response.message + refute_nil response.params['access_token'] + refute_nil response.params['encryption_key'] + end + + def test_unsuccessful_purchase_with_merchant_discredited + set_credentials! + @gateway.options[:encryption_uuid] = '7c82f46e-64f7-4745-9c60-335a689b8e90' + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_match %r(contato), response.message + end + + def test_unsuccessful_purchase_with_insuffieicent_funds + set_credentials! + @gateway.options[:encryption_uuid] = 'a36aa740-d505-4d47-8aa6-6c31c7526a68' + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_match %r(insuficiente), response.message + end + + def test_unsuccessful_purchase_with_invalid_fields + set_credentials! + @gateway.options[:encryption_uuid] = 'd7aff4a6-1ea1-4e74-b81a-934589385958' + response = @gateway.purchase(@amount, @declined_card, @options) + + assert_failure response + assert_match %r{Erro}, response.message + end + + def test_unsuccessful_purchase_with_blocked_card + set_credentials! + @gateway.options[:encryption_uuid] = 'd2a0350d-e872-47bf-a543-2d36c2ad693e' + response = @gateway.purchase(@amount, @declined_card, @options) + + assert_failure response + assert_match %r(Bloqueado), response.message + end + + def test_successful_purchase_with_geolocalitation + set_credentials! + options = { + geo_longitude: '10.451526', + geo_latitude: '51.165691', + uuid: '53141521-afc8-4a08-af0c-f0382aef43c1' + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(options)) + assert_success response + assert_match %r(Confirmada), response.message + end + + def test_invalid_login + gateway = AleloGateway.new(client_id: 'asdfghj', client_secret: '1234rtytre') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match %r{invalid_client}, response.message + end + + def test_transcript_scrubbing + set_credentials! + transcript = capture_transcript(@gateway) do + @gateway.options[:encryption_uuid] = '53141521-afc8-4a08-af0c-f0382aef43c1' + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@gateway.options[:client_id], transcript) + assert_scrubbed(@gateway.options[:client_secret], transcript) + end + + def test_successful_refund + set_credentials! + response = @gateway.refund(@amount, '990a39dd-3df2-46a5-89ac-012cca00ef0b#def456', {}) + + assert_success response + assert_match %r{Estornada}, response.message + end + + def test_failure_refund_with_invalid_uuid + set_credentials! + response = @gateway.refund(@amount, '7f723387-d449-4c6c-aca3-9a583689dc34', {}) + + assert_failure response + end + + private + + def set_credentials! + if AleloCredentials.instance.access_token.nil? + credentials = @gateway.send :ensure_credentials, {} + AleloCredentials.instance.access_token = credentials[:access_token] + AleloCredentials.instance.key = credentials[:key] + AleloCredentials.instance.uuid = credentials[:uuid] + end + + @gateway.options[:access_token] = AleloCredentials.instance.access_token + @gateway.options[:encryption_key] = AleloCredentials.instance.key + @gateway.options[:encryption_uuid] = AleloCredentials.instance.uuid + end +end + +# A simple singleton so an access token and key can +# be shared among several tests +class AleloCredentials + include Singleton + + attr_accessor :access_token, :key, :uuid +end diff --git a/test/remote/gateways/remote_alelo_test_certification.rb b/test/remote/gateways/remote_alelo_test_certification.rb new file mode 100644 index 00000000000..f6a2cb0d771 --- /dev/null +++ b/test/remote/gateways/remote_alelo_test_certification.rb @@ -0,0 +1,136 @@ +require 'test_helper' +require 'singleton' + +class RemoteAleloTestCertification < Test::Unit::TestCase + def setup + @gateway = AleloGateway.new(fixtures(:alelo_certification)) + @amount = 1000 + @cc_alimentacion = credit_card('5098870005467012', { + month: 8, + year: 2027, + first_name: 'Longbob', + last_name: 'Longsen', + verification_value: 747, + brand: 'mc' + }) + @cc_snack = credit_card('5067580024660011', { + month: 8, + year: 2027, + first_name: 'Longbob', + last_name: 'Longsen', + verification_value: 576, + brand: 'mc' + }) + @options = { + order_id: SecureRandom.uuid, + establishment_code: '1040471819', + sub_merchant_mcc: '5499', + player_identification: '7', + description: 'Store Purchase', + external_trace_number: '123456' + } + end + + def test_failure_purchase_with_wrong_cvv_ct05 + set_credentials! + @cc_snack.verification_value = 999 + response = @gateway.purchase(@amount, @cc_snack, @options) + + assert_failure response + assert_match %r{incorreto}i, response.message + end + + def test_failure_with_incomplete_required_options_ct06 + set_credentials! + @options.delete(:establishment_code) + response = @gateway.purchase(@amount, @cc_alimentacion, @options) + + assert_failure response + assert_match %r{Erro ao validar dados}i, response.message + end + + def test_failure_refund_with_non_existent_uuid_ct07 + set_credentials! + response = @gateway.refund(@amount, SecureRandom.uuid, {}) + + assert_failure response + assert_match %r{RequestId informado, não encontrado!}, response.message + end + + def test_successful_purchase_with_alimentazao_cc_ct08 + response = @gateway.purchase(@amount, @cc_alimentacion, @options) + + assert_success response + assert_match %r{confirmada}i, response.message + end + + # Testing High value transaction disables the test credit card + # + # def test_successful_purchase_with_alimentazao_cc_ct08_B_high_value + # response = @gateway.purchase(10_000_000, @cc_alimentacion, @options) + # assert_failure response + # end + + def test_successful_refund_ct09 + set_credentials! + purchase = @gateway.purchase(@amount, @cc_alimentacion, @options) + response = @gateway.refund(@amount, purchase.authorization, {}) + + assert_success response + assert_match %r{Transação Estornada com sucesso}, response.message + end + + def test_failure_with_non_exitent_establishment_code_ct10 + @options[:establishment_code] = '0987654321' + @options[:sub_merchant_mcc] = '5411' + + response = @gateway.purchase(@amount, @cc_alimentacion, @options) + + assert_failure response + assert_match %r{no adquirente}i, response.message + end + + def test_failure_when_cc_expired_ct11 + @cc_alimentacion.year = 2020 + set_credentials! + + response = @gateway.purchase(@amount, @cc_alimentacion, @options) + + assert_failure response + end + + def test_failure_refund_with_purchase_already_refunded_ct12 + set_credentials! + purchase = @gateway.purchase(@amount, @cc_alimentacion, @options) + assert_success purchase + + response = @gateway.refund(@amount, purchase.authorization, {}) + assert_success response + + response = @gateway.refund(@amount, purchase.authorization, {}) + assert_failure response + end + + private + + def set_credentials! + if AleloCredentials.instance.access_token.nil? + credentials = @gateway.send :ensure_credentials, {} + AleloCredentials.instance.access_token = credentials[:access_token] + AleloCredentials.instance.key = credentials[:key] + AleloCredentials.instance.uuid = credentials[:uuid] + end + + @gateway.options[:access_token] = AleloCredentials.instance.access_token + @gateway.options[:encryption_key] = AleloCredentials.instance.key + @gateway.options[:encryption_uuid] = AleloCredentials.instance.uuid + end +end + +# A simple singleton so an access token and key can +# be shared among several tests +class AleloCredentials + include Singleton + + attr_accessor :access_token, :key, :uuid +end diff --git a/test/remote/gateways/remote_authorize_net_apple_pay_test.rb b/test/remote/gateways/remote_authorize_net_apple_pay_test.rb index 0cd7a16fe8f..836af912cb3 100644 --- a/test/remote/gateways/remote_authorize_net_apple_pay_test.rb +++ b/test/remote/gateways/remote_authorize_net_apple_pay_test.rb @@ -82,9 +82,11 @@ def apple_pay_payment_token(options = {}) transaction_identifier: 'uniqueidentifier123' }.update(options) - ActiveMerchant::Billing::ApplePayPaymentToken.new(defaults[:payment_data], + ActiveMerchant::Billing::ApplePayPaymentToken.new( + defaults[:payment_data], payment_instrument_name: defaults[:payment_instrument_name], payment_network: defaults[:payment_network], - transaction_identifier: defaults[:transaction_identifier]) + transaction_identifier: defaults[:transaction_identifier] + ) end end diff --git a/test/remote/gateways/remote_authorize_net_test.rb b/test/remote/gateways/remote_authorize_net_test.rb index 339776c9766..e6388238550 100644 --- a/test/remote/gateways/remote_authorize_net_test.rb +++ b/test/remote/gateways/remote_authorize_net_test.rb @@ -9,6 +9,17 @@ def setup @check = check @declined_card = credit_card('400030001111222') + @payment_token = network_tokenization_credit_card( + '4242424242424242', + payment_cryptogram: 'dGVzdGNyeXB0b2dyYW1YWFhYWFhYWFhYWFg9PQ==', + brand: 'visa', + eci: '05', + month: '09', + year: '2030', + first_name: 'Longbob', + last_name: 'Longsen' + ) + @options = { order_id: '1', email: 'anet@example.com', @@ -56,6 +67,26 @@ def test_successful_purchase assert response.authorization end + def test_successful_purchase_with_google_pay + @payment_token.source = :google_pay + response = @gateway.purchase(@amount, @payment_token, @options) + + assert_success response + assert response.test? + assert_equal 'This transaction has been approved', response.message + assert response.authorization + end + + def test_successful_purchase_with_apple_pay + @payment_token.source = :apple_pay + response = @gateway.purchase(@amount, @payment_token, @options) + + assert_success response + assert response.test? + assert_equal 'This transaction has been approved', response.message + assert response.authorization + end + def test_successful_purchase_with_minimal_options response = @gateway.purchase(@amount, @credit_card, duplicate_window: 0, email: 'anet@example.com', billing_address: address) assert_success response @@ -383,11 +414,76 @@ def test_successful_authorization_with_moto_retail_type assert response.authorization end + def test_successful_purchase_with_phone_number + @options[:billing_address][:phone] = nil + @options[:billing_address][:phone_number] = '5554443210' + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert response.test? + assert_equal 'This transaction has been approved', response.message + assert response.authorization + end + def test_successful_verify response = @gateway.verify(@credit_card, @options) assert_success response assert_equal 'This transaction has been approved', response.message - assert_success response.responses.last, 'The void should succeed' + assert_equal response.responses.count, 2 + end + + def test_successful_verify_with_no_address + @options[:billing_address] = nil + response = @gateway.verify(@credit_card, @options) + + assert_success response + assert_equal 'This transaction has been approved', response.message + assert_equal response.responses.count, 2 + end + + def test_successful_verify_with_verify_amount_and_billing_address + @options[:verify_amount] = 1 + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'This transaction has been approved', response.message + assert_equal response.responses.count, 2 + end + + def test_successful_verify_after_store_with_custom_verify_amount + @options[:verify_amount] = 1 + assert store = @gateway.store(@credit_card, @options) + assert_success store + response = @gateway.verify(store.authorization, @options) + assert_success response + assert_equal response.responses.count, 2 + end + + def test_successful_verify_with_apple_pay + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: '111111111100cryptogram') + response = @gateway.verify(credit_card, @options) + assert_success response + assert_equal 'This transaction has been approved', response.message + end + + def test_successful_verify_with_check + response = @gateway.verify(@check, @options) + assert_success response + assert_equal 'This transaction has been approved', response.message + end + + def test_successful_verify_with_nil_custom_verify_amount + @options[:verify_amount] = nil + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'This transaction has been approved', response.message + end + + def test_verify_tpt_with_custom_verify_amount_and_no_address + @options[:verify_amount] = 100 + assert store = @gateway.store(@credit_card, @options) + assert_success store + @options[:billing_address] = nil + response = @gateway.verify(store.authorization, @options) + assert_success response end def test_failed_verify @@ -738,6 +834,18 @@ def test_successful_echeck_refund assert_match %r{The transaction cannot be found}, refund.message, 'Only allowed to refund transactions that have settled. This is the best we can do for now testing wise.' end + def test_successful_echeck_refund_truncates_long_account_name + check_with_long_name = check(name: 'Michelangelo Donatello-Raphael') + purchase = @gateway.purchase(@amount, check_with_long_name, @options) + assert_success purchase + + @options.update(first_name: check_with_long_name.first_name, last_name: check_with_long_name.last_name, routing_number: check_with_long_name.routing_number, + account_number: check_with_long_name.account_number, account_type: check_with_long_name.account_type) + refund = @gateway.refund(@amount, purchase.authorization, @options) + assert_failure refund + assert_match %r{The transaction cannot be found}, refund.message, 'Only allowed to refund transactions that have settled. This is the best we can do for now testing wise.' + end + def test_failed_credit response = @gateway.credit(@amount, @declined_card, @options) assert_failure response @@ -763,9 +871,11 @@ def test_dump_transcript end def test_successful_authorize_and_capture_with_network_tokenization - credit_card = network_tokenization_credit_card('4000100011112224', + credit_card = network_tokenization_credit_card( + '4000100011112224', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - verification_value: nil) + verification_value: nil + ) auth = @gateway.authorize(@amount, credit_card, @options) assert_success auth assert_equal 'This transaction has been approved', auth.message @@ -775,9 +885,11 @@ def test_successful_authorize_and_capture_with_network_tokenization end def test_successful_refund_with_network_tokenization - credit_card = network_tokenization_credit_card('4000100011112224', + credit_card = network_tokenization_credit_card( + '4000100011112224', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - verification_value: nil) + verification_value: nil + ) purchase = @gateway.purchase(@amount, credit_card, @options) assert_success purchase @@ -790,9 +902,11 @@ def test_successful_refund_with_network_tokenization end def test_successful_credit_with_network_tokenization - credit_card = network_tokenization_credit_card('4000100011112224', + credit_card = network_tokenization_credit_card( + '4000100011112224', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - verification_value: nil) + verification_value: nil + ) response = @gateway.credit(@amount, credit_card, @options) assert_success response @@ -801,10 +915,12 @@ def test_successful_credit_with_network_tokenization end def test_network_tokenization_transcript_scrubbing - credit_card = network_tokenization_credit_card('4111111111111111', + credit_card = network_tokenization_credit_card( + '4111111111111111', brand: 'visa', eci: '05', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) transcript = capture_transcript(@gateway) do @gateway.authorize(@amount, credit_card, @options) diff --git a/test/remote/gateways/remote_barclaycard_smartpay_test.rb b/test/remote/gateways/remote_barclaycard_smartpay_test.rb index ac59135ddba..1839dfe8ea9 100644 --- a/test/remote/gateways/remote_barclaycard_smartpay_test.rb +++ b/test/remote/gateways/remote_barclaycard_smartpay_test.rb @@ -100,10 +100,12 @@ def setup } } - @avs_credit_card = credit_card('4400000000000008', + @avs_credit_card = credit_card( + '4400000000000008', month: 8, year: 2018, - verification_value: 737) + verification_value: 737 + ) @avs_address = @options.clone @avs_address.update(billing_address: { @@ -158,25 +160,31 @@ def test_failed_purchase end def test_successful_purchase_with_unusual_address - response = @gateway.purchase(@amount, + response = @gateway.purchase( + @amount, @credit_card, - @options_with_alternate_address) + @options_with_alternate_address + ) assert_success response assert_equal '[capture-received]', response.message end def test_successful_purchase_with_house_number_and_street - response = @gateway.purchase(@amount, + response = @gateway.purchase( + @amount, @credit_card, - @options.merge(street: 'Top Level Drive', house_number: '100')) + @options.merge(street: 'Top Level Drive', house_number: '100') + ) assert_success response assert_equal '[capture-received]', response.message end def test_successful_purchase_with_no_address - response = @gateway.purchase(@amount, + response = @gateway.purchase( + @amount, @credit_card, - @options_with_no_address) + @options_with_no_address + ) assert_success response assert_equal '[capture-received]', response.message end diff --git a/test/remote/gateways/remote_beanstream_test.rb b/test/remote/gateways/remote_beanstream_test.rb index 93b799e4ac5..cc950418571 100644 --- a/test/remote/gateways/remote_beanstream_test.rb +++ b/test/remote/gateways/remote_beanstream_test.rb @@ -403,6 +403,35 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:api_key], clean_transcript) end + def test_successful_authorize_with_3ds_v1_options + @options[:three_d_secure] = { + version: '1.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + xid: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } + assert response = @gateway.purchase(@amount, @visa, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_authorize_with_3ds_v2_options + @options[:three_d_secure] = { + version: '2.2.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'Y', + authentication_response_status: 'Y' + } + + assert response = @gateway.purchase(@amount, @visa, @options) + assert_success response + assert_equal 'Approved', response.message + end + private def generate_single_use_token(credit_card) diff --git a/test/remote/gateways/remote_blue_snap_test.rb b/test/remote/gateways/remote_blue_snap_test.rb index 011a22be547..a984beeeb1c 100644 --- a/test/remote/gateways/remote_blue_snap_test.rb +++ b/test/remote/gateways/remote_blue_snap_test.rb @@ -249,6 +249,40 @@ def test_successful_purchase_with_3ds2_auth assert_equal 'Success', response.message end + def test_successful_purchase_for_stored_credentials_with_cit + cit_stored_credentials = { + initiator: 'cardholder' + } + response = @gateway.purchase(@amount, @three_ds_visa_card, @options_3ds2.merge({ stored_credential: cit_stored_credentials })) + assert_success response + assert_equal 'Success', response.message + + cit_stored_credentials = { + initiator: 'cardholder', + network_transaction_id: response.params['original-network-transaction-id'] + } + response = @gateway.purchase(@amount, @three_ds_visa_card, @options_3ds2.merge({ stored_credential: cit_stored_credentials })) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_for_stored_credentials_with_mit + mit_stored_credentials = { + initiator: 'merchant' + } + response = @gateway.purchase(@amount, @three_ds_visa_card, @options_3ds2.merge({ stored_credential: mit_stored_credentials })) + assert_success response + assert_equal 'Success', response.message + + mit_stored_credentials = { + initiator: 'merchant', + network_transaction_id: response.params['original-network-transaction-id'] + } + response = @gateway.purchase(@amount, @three_ds_visa_card, @options_3ds2.merge({ stored_credential: mit_stored_credentials })) + assert_success response + assert_equal 'Success', response.message + end + def test_successful_purchase_with_currency response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'CAD')) assert_success response @@ -413,6 +447,13 @@ def test_successful_authorize_and_partial_capture assert_equal 'Success', capture.message end + def test_successful_authorize_with_descriptor_phone_number + response = @gateway.authorize(@amount, @credit_card, @options.merge({ descriptor_phone_number: '321-321-4321' })) + + assert_success response + assert_equal 'Success', response.message + end + def test_successful_authorize_and_capture_with_3ds2_auth auth = @gateway.authorize(@amount, @three_ds_master_card, @options_3ds2) assert_success auth @@ -449,6 +490,7 @@ def test_successful_refund assert refund = @gateway.refund(@amount, purchase.authorization, @refund_options) assert_success refund assert_equal 'Success', refund.message + assert_not_nil refund.authorization end def test_successful_refund_with_merchant_id diff --git a/test/remote/gateways/remote_borgun_test.rb b/test/remote/gateways/remote_borgun_test.rb index eddde1d769a..1108632f95f 100644 --- a/test/remote/gateways/remote_borgun_test.rb +++ b/test/remote/gateways/remote_borgun_test.rb @@ -8,7 +8,8 @@ def setup @gateway = BorgunGateway.new(fixtures(:borgun)) @amount = 100 - @credit_card = credit_card('5587402000012011', year: 2022, month: 9, verification_value: 415) + @credit_card = credit_card('5587402000012011', year: 2027, month: 9, verification_value: 415) + @frictionless_3ds_card = credit_card('5455330200000016', verification_value: 415, month: 9, year: 2027) @declined_card = credit_card('4155520000000002') @options = { @@ -28,6 +29,21 @@ def test_successful_purchase assert_equal 'Succeeded', response.message end + def test_successful_preauth_3ds + response = @gateway.purchase(@amount, @credit_card, @options.merge({ redirect_url: 'http://localhost/index.html', apply_3d_secure: '1' })) + assert_success response + assert_equal 'Succeeded', response.message + assert_not_nil response.params['redirecttoacsform'] + end + + def test_successful_preauth_frictionless_3ds + response = @gateway.purchase(@amount, @frictionless_3ds_card, @options.merge({ redirect_url: 'http://localhost/index.html', apply_3d_secure: '1' })) + assert_success response + assert_equal 'Succeeded', response.message + assert_nil response.params['redirecttoacsform'] + assert_equal response.params['threedsfrictionless'], 'A' + end + def test_successful_purchase_usd response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'USD')) assert_success response @@ -171,19 +187,19 @@ def test_failed_void # This test does not consistently pass. When run multiple times within 1 minute, # an ActiveMerchant::ConnectionError() # exception is raised. - def test_invalid_login - gateway = BorgunGateway.new( - processor: '0', - merchant_id: '0', - username: 'not', - password: 'right' - ) - authentication_exception = assert_raise ActiveMerchant::ResponseError, 'Failed with 401 [ISS.0084.9001] Invalid credentials' do - gateway.purchase(@amount, @credit_card, @options) - end - assert response = authentication_exception.response - assert_match(/Access Denied/, response.body) - end + # def test_invalid_login + # gateway = BorgunGateway.new( + # processor: '0', + # merchant_id: '0', + # username: 'not', + # password: 'right' + # ) + # authentication_exception = assert_raise ActiveMerchant::ResponseError, 'Failed with 401 [ISS.0084.9001] Invalid credentials' do + # gateway.purchase(@amount, @credit_card, @options) + # end + # assert response = authentication_exception.response + # assert_match(/Access Denied/, response.body) + # end def test_transcript_scrubbing transcript = capture_transcript(@gateway) do diff --git a/test/remote/gateways/remote_braintree_blue_test.rb b/test/remote/gateways/remote_braintree_blue_test.rb index 098515cb04e..695315f7308 100644 --- a/test/remote/gateways/remote_braintree_blue_test.rb +++ b/test/remote/gateways/remote_braintree_blue_test.rb @@ -31,6 +31,12 @@ def setup }, ach_mandate: ach_mandate } + + @nt_credit_card = network_tokenization_credit_card('4111111111111111', + brand: 'visa', + eci: '05', + source: :network_token, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') end def test_credit_card_details_on_store @@ -54,6 +60,13 @@ def test_successful_authorize assert_equal 'authorized', response.params['braintree_transaction']['status'] end + def test_successful_authorize_with_nt + assert response = @gateway.authorize(@amount, @nt_credit_card, @options) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'authorized', response.params['braintree_transaction']['status'] + end + def test_successful_authorize_with_nil_and_empty_billing_address_options credit_card = credit_card('5105105105105100') options = { @@ -83,6 +96,13 @@ def test_masked_card_number assert_equal('510510', response.params['braintree_transaction']['credit_card_details']['bin']) end + def test_successful_setup_purchase + assert response = @gateway.setup_purchase + assert_success response + assert_equal 'Client token created', response.message + assert_not_nil response.params['client_token'] + end + def test_successful_authorize_with_order_id assert response = @gateway.authorize(@amount, @credit_card, order_id: '123') assert_success response @@ -194,6 +214,36 @@ def test_successful_purchase_sending_risk_data assert_success response end + def test_successful_purchase_with_paypal_options + options = @options.merge( + paypal_custom_field: 'abc', + paypal_description: 'shoes' + ) + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + # Follow instructions found at https://developer.paypal.com/braintree/articles/guides/payment-methods/venmo#multiple-profiles + # for sandbox control panel https://sandbox.braintreegateway.com/login to create a venmo profile. + # Insert your Profile Id into fixtures. + def test_successful_purchase_with_venmo_profile_id + options = @options.merge(venmo_profile_id: fixtures(:braintree_blue)[:venmo_profile_id], payment_method_nonce: 'fake-venmo-account-nonce') + assert response = @gateway.purchase(@amount, 'fake-venmo-account-nonce', options) + assert_success response + end + + def test_successful_partial_capture + options = @options.merge(venmo_profile_id: fixtures(:braintree_blue)[:venmo_profile_id], payment_method_nonce: 'fake-venmo-account-nonce') + assert auth = @gateway.authorize(@amount, 'fake-venmo-account-nonce', options) + assert_success auth + assert_equal '1000 Approved', auth.message + assert auth.authorization + assert capture_one = @gateway.capture(50, auth.authorization, { partial_capture: true }) + assert_success capture_one + assert capture_two = @gateway.capture(50, auth.authorization, { partial_capture: true }) + assert_success capture_two + end + def test_successful_verify assert response = @gateway.verify(@credit_card, @options) assert_success response @@ -433,8 +483,7 @@ def test_cvv_no_match end def test_successful_purchase_with_email - assert response = @gateway.purchase(@amount, @credit_card, - email: 'customer@example.com') + assert response = @gateway.purchase(@amount, @credit_card, email: 'customer@example.com') assert_success response transaction = response.params['braintree_transaction'] assert_equal 'customer@example.com', transaction['customer_details']['email'] @@ -454,6 +503,15 @@ def test_successful_purchase_with_phone_from_address assert_equal '(555)555-5555', transaction['customer_details']['phone'] end + def test_successful_purchase_with_phone_number_from_address + @options[:billing_address][:phone] = nil + @options[:billing_address][:phone_number] = '9191231234' + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + transaction = response.params['braintree_transaction'] + assert_equal '9191231234', transaction['customer_details']['phone'] + end + def test_successful_purchase_with_skip_advanced_fraud_checking_option assert response = @gateway.purchase(@amount, @credit_card, @options.merge(skip_advanced_fraud_checking: true)) assert_success response @@ -524,9 +582,7 @@ def test_purchase_with_transaction_source def test_purchase_using_specified_payment_method_token assert response = @gateway.store( - credit_card('4111111111111111', - first_name: 'Old First', last_name: 'Old Last', - month: 9, year: 2012), + credit_card('4111111111111111', first_name: 'Old First', last_name: 'Old Last', month: 9, year: 2012), email: 'old@example.com', phone: '321-654-0987' ) @@ -558,9 +614,12 @@ def test_successful_purchase_with_addresses zip: '60103', country_name: 'Mexico' } - assert response = @gateway.purchase(@amount, @credit_card, + assert response = @gateway.purchase( + @amount, + @credit_card, billing_address: billing_address, - shipping_address: shipping_address) + shipping_address: shipping_address + ) assert_success response transaction = response.params['braintree_transaction'] assert_equal '1 E Main St', transaction['billing_details']['street_address'] @@ -579,17 +638,18 @@ def test_successful_purchase_with_addresses assert_equal 'Mexico', transaction['shipping_details']['country_name'] end - def test_successful_purchase_with_three_d_secure_pass_thru - three_d_secure_params = { version: '2.0', cavv: 'cavv', eci: '02', ds_transaction_id: 'trans_id', cavv_algorithm: 'algorithm', directory_response_status: 'directory', authentication_response_status: 'auth' } - response = @gateway.purchase(@amount, @credit_card, - three_d_secure: three_d_secure_params) + def test_successful_purchase_with_three_d_secure_pass_thru_and_sca_exemption + options = { + three_ds_exemption_type: 'low_value', + three_d_secure: { version: '2.0', cavv: 'cavv', eci: '02', ds_transaction_id: 'trans_id', cavv_algorithm: 'algorithm', directory_response_status: 'directory', authentication_response_status: 'auth' } + } + response = @gateway.purchase(@amount, @credit_card, options) assert_success response end def test_successful_purchase_with_some_three_d_secure_pass_thru_fields three_d_secure_params = { version: '2.0', cavv: 'cavv', eci: '02', ds_transaction_id: 'trans_id' } - response = @gateway.purchase(@amount, @credit_card, - three_d_secure: three_d_secure_params) + response = @gateway.purchase(@amount, @credit_card, three_d_secure: three_d_secure_params) assert_success response end @@ -604,7 +664,13 @@ def test_unsuccessful_purchase_validation_error assert response = @gateway.purchase(@amount, credit_card('51051051051051000')) assert_failure response assert_match %r{Credit card number is invalid\. \(81715\)}, response.message - assert_equal({ 'processor_response_code' => '91577' }, response.params['braintree_transaction']) + assert_equal('91577', response.params['braintree_transaction']['processor_response_code']) + end + + def test_unsuccessful_purchase_with_additional_processor_response + assert response = @gateway.purchase(204700, @credit_card) + assert_failure response + assert_equal('2047 : Call Issuer. Pick Up Card.', response.params['braintree_transaction']['additional_processor_response']) end def test_authorize_and_capture @@ -617,27 +683,12 @@ def test_authorize_and_capture end def test_authorize_and_capture_with_apple_pay_card - credit_card = network_tokenization_credit_card('4111111111111111', + credit_card = network_tokenization_credit_card( + '4111111111111111', brand: 'visa', eci: '05', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') - - assert auth = @gateway.authorize(@amount, credit_card, @options) - assert_success auth - assert_equal '1000 Approved', auth.message - assert auth.authorization - assert capture = @gateway.capture(@amount, auth.authorization) - assert_success capture - end - - def test_authorize_and_capture_with_android_pay_card - credit_card = network_tokenization_credit_card('4111111111111111', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - month: '01', - year: '2024', - source: :android_pay, - transaction_id: '123456789', - eci: '05') + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) assert auth = @gateway.authorize(@amount, credit_card, @options) assert_success auth @@ -648,13 +699,15 @@ def test_authorize_and_capture_with_android_pay_card end def test_authorize_and_capture_with_google_pay_card - credit_card = network_tokenization_credit_card('4111111111111111', + credit_card = network_tokenization_credit_card( + '4111111111111111', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', month: '01', year: '2024', source: :google_pay, transaction_id: '123456789', - eci: '05') + eci: '05' + ) assert auth = @gateway.authorize(@amount, credit_card, @options) assert_success auth @@ -705,7 +758,7 @@ def test_failed_void assert failed_void = @gateway.void(auth.authorization) assert_failure failed_void assert_match('Transaction can only be voided if status is authorized', failed_void.message) - assert_equal({ 'processor_response_code' => '91504' }, failed_void.params['braintree_transaction']) + assert_equal('91504', failed_void.params['braintree_transaction']['processor_response_code']) end def test_failed_capture_with_invalid_transaction_id @@ -765,9 +818,7 @@ def test_unstore_with_delete_method def test_successful_update assert response = @gateway.store( - credit_card('4111111111111111', - first_name: 'Old First', last_name: 'Old Last', - month: 9, year: 2012), + credit_card('4111111111111111', first_name: 'Old First', last_name: 'Old Last', month: 9, year: 2012), email: 'old@example.com', phone: '321-654-0987' ) @@ -786,9 +837,7 @@ def test_successful_update assert response = @gateway.update( customer_vault_id, - credit_card('5105105105105100', - first_name: 'New First', last_name: 'New Last', - month: 10, year: 2014), + credit_card('5105105105105100', first_name: 'New First', last_name: 'New Last', month: 10, year: 2014), email: 'new@example.com', phone: '987-765-5432' ) @@ -881,6 +930,25 @@ def test_successful_credit_with_merchant_account_id assert_equal 'submitted_for_settlement', response.params['braintree_transaction']['status'] end + def test_failed_credit_with_merchant_account_id + assert response = @gateway.credit(@declined_amount, credit_card('4000111111111115'), merchant_account_id: fixtures(:braintree_blue)[:merchant_account_id]) + assert_failure response + assert_equal '2000 Do Not Honor', response.message + assert_equal '2000 : Do Not Honor', response.params['braintree_transaction']['additional_processor_response'] + end + + def test_successful_credit_using_card_token + assert response = @gateway.store(@credit_card) + assert_success response + assert_equal 'OK', response.message + credit_card_token = response.params['credit_card_token'] + + assert response = @gateway.credit(@amount, credit_card_token, { merchant_account_id: fixtures(:braintree_blue)[:merchant_account_id], payment_method_token: true }) + assert_success response, 'You must specify a valid :merchant_account_id key in your fixtures.yml AND get credits enabled in your Sandbox account for this to pass.' + assert_equal '1002 Processed', response.message + assert_equal 'submitted_for_settlement', response.params['braintree_transaction']['status'] + end + def test_successful_authorize_with_merchant_account_id assert response = @gateway.authorize(@amount, @credit_card, merchant_account_id: fixtures(:braintree_blue)[:merchant_account_id]) assert_success response, 'You must specify a valid :merchant_account_id key in your fixtures.yml for this to pass.' @@ -894,25 +962,31 @@ def test_authorize_with_descriptor end def test_authorize_with_travel_data - assert auth = @gateway.authorize(@amount, @credit_card, + assert auth = @gateway.authorize( + @amount, + @credit_card, travel_data: { travel_package: 'flight', departure_date: '2050-07-22', lodging_check_in_date: '2050-07-22', lodging_check_out_date: '2050-07-25', lodging_name: 'Best Hotel Ever' - }) + } + ) assert_success auth end def test_authorize_with_lodging_data - assert auth = @gateway.authorize(@amount, @credit_card, + assert auth = @gateway.authorize( + @amount, + @credit_card, lodging_data: { folio_number: 'ABC123', check_in_date: '2050-12-22', check_out_date: '2050-12-25', room_rate: '80.00' - }) + } + ) assert_success auth end @@ -1148,6 +1222,29 @@ def test_successful_purchase_with_the_same_bank_account_several_times assert_equal '4002 Settlement Pending', response.message end + def test_successful_purchase_with_processor_authorization_code + assert response = @gateway.purchase(@amount, @credit_card) + assert_success response + assert_equal '1000 Approved', response.message + assert_not_nil response.params['braintree_transaction']['processor_authorization_code'] + end + + def test_successful_purchase_with_with_prepaid_debit_issuing_bank + assert response = @gateway.purchase(@amount, @credit_card) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'Unknown', response.params['braintree_transaction']['credit_card_details']['prepaid'] + assert_equal 'Unknown', response.params['braintree_transaction']['credit_card_details']['debit'] + assert_equal 'Unknown', response.params['braintree_transaction']['credit_card_details']['issuing_bank'] + end + + def test_successful_purchase_with_global_id + assert response = @gateway.purchase(@amount, @credit_card) + assert_success response + assert_equal '1000 Approved', response.message + assert_not_nil response.params['braintree_transaction']['payment_receipt']['global_id'] + end + def test_unsucessful_purchase_using_a_bank_account_token_not_verified bank_account = check({ account_number: '1000000002', routing_number: '011000015' }) response = @gateway.store(bank_account, @options.merge(@check_required_options)) diff --git a/test/remote/gateways/remote_card_connect_test.rb b/test/remote/gateways/remote_card_connect_test.rb index 7f9e5b026e2..68301145fd6 100644 --- a/test/remote/gateways/remote_card_connect_test.rb +++ b/test/remote/gateways/remote_card_connect_test.rb @@ -134,11 +134,13 @@ def test_successful_purchase_with_user_fields assert_equal 'Approval Queued for Capture', response.message end - def test_successful_purchase_3DS + def test_successful_purchase_three_ds three_ds_options = @options.merge( - secure_flag: 'se3453', - secure_value: '233frdf', - secure_xid: '334ef34' + three_d_secure: { + eci: 'se3453', + cavv: 'AJkBByEyYgAAAASwgmEodQAAAAA=', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==' + } ) response = @gateway.purchase(@amount, @credit_card, three_ds_options) assert_success response @@ -152,6 +154,31 @@ def test_successful_purchase_with_profile assert_success purchase_response end + def test_successful_purchase_using_stored_credential_framework + stored_credential_options = { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'merchant' + } + response = @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + assert_success response + assert_equal response.params['cof'], 'M' + + stored_credential_options = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant' + } + response = @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + assert_success response + assert_equal response.params['cof'], 'M' + end + + def test_successful_purchase_with_telephonic_ecomind + response = @gateway.purchase(@amount, @credit_card, @options.merge({ ecomind: 'T' })) + assert_success response + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -199,22 +226,27 @@ def test_failed_echeck_purchase assert_equal 'Invalid card', response.message end - def test_successful_refund - purchase = @gateway.purchase(@amount, @credit_card, @options) - assert_success purchase - - assert refund = @gateway.refund(@amount, purchase.authorization) - assert_success refund - assert_equal 'Approval', refund.message - end - - def test_partial_refund - purchase = @gateway.purchase(@amount, @credit_card, @options) - assert_success purchase - - assert refund = @gateway.refund(@amount - 1, purchase.authorization) - assert_success refund - end + # A transaction cannot be refunded before settlement so these tests will + # fail with the following response, to properly test refunds create a purchase + # save the reference and test the next day, check: + # https://cardconnect.com/launchpointe/running-a-business/payment-processing-101#how_long_it_takes + # + # def test_successful_refund + # purchase = @gateway.purchase(@amount, @credit_card, @options) + # assert_success purchase + # + # assert refund = @gateway.refund(@amount, purchase.authorization) + # assert_success refund + # assert_equal 'Approval', refund.message + # end + # + # def test_partial_refund + # purchase = @gateway.purchase(@amount, @credit_card, @options) + # assert_success purchase + # + # assert refund = @gateway.refund(@amount - 1, purchase.authorization) + # assert_success refund + # end def test_failed_refund response = @gateway.refund(@amount, @invalid_txn) diff --git a/test/remote/gateways/remote_card_stream_test.rb b/test/remote/gateways/remote_card_stream_test.rb index b37ecdf90d2..48fe71952ec 100644 --- a/test/remote/gateways/remote_card_stream_test.rb +++ b/test/remote/gateways/remote_card_stream_test.rb @@ -6,33 +6,43 @@ def setup @gateway = CardStreamGateway.new(fixtures(:card_stream)) - @amex = credit_card('374245455400001', + @amex = credit_card( + '374245455400001', month: '12', year: Time.now.year + 1, verification_value: '4887', - brand: :american_express) + brand: :american_express + ) - @mastercard = credit_card('5301250070000191', + @mastercard = credit_card( + '5301250070000191', month: '12', year: Time.now.year + 1, verification_value: '419', - brand: :master) + brand: :master + ) - @visacreditcard = credit_card('4929421234600821', + @visacreditcard = credit_card( + '4929421234600821', month: '12', year: Time.now.year + 1, verification_value: '356', - brand: :visa) + brand: :visa + ) - @visadebitcard = credit_card('4539791001730106', + @visadebitcard = credit_card( + '4539791001730106', month: '12', year: Time.now.year + 1, verification_value: '289', - brand: :visa) + brand: :visa + ) - @declined_card = credit_card('4000300011112220', + @declined_card = credit_card( + '4000300011112220', month: '9', - year: Time.now.year + 1) + year: Time.now.year + 1 + ) @amex_options = { billing_address: { @@ -109,10 +119,23 @@ def setup ip: '1.1.1.1' } - @three_ds_enrolled_card = credit_card('4012001037141112', + @three_ds_enrolled_card = credit_card( + '4012001037141112', month: '12', year: '2020', - brand: :visa) + brand: :visa + ) + + @visacredit_three_ds_options = { + threeds_required: true, + three_d_secure: { + enrolled: 'true', + authentication_response_status: 'Y', + eci: '05', + cavv: 'Y2FyZGluYWxjb21tZXJjZWF1dGg', + xid: '00000000000004717472' + } + } end def test_successful_visacreditcard_authorization_and_capture @@ -420,6 +443,38 @@ def test_successful_3dsecure_auth assert !response.params['threeDSPaReq'].blank? end + def test_3dsecure2_auth_authenticated_card + assert response = @gateway.authorize(1202, @visacreditcard, @visacredit_options.merge(@visacredit_three_ds_options)) + assert_equal 'APPROVED', response.message + assert_equal '0', response.params['responseCode'] + assert_equal 'Success', response.params['threeDSResponseMessage'] + assert response.success? + assert response.test? + assert !response.authorization.blank? + end + + def test_3dsecure2_auth_not_authenticated_card + @visacredit_three_ds_options[:three_d_secure][:authentication_response_status] = 'N' + assert response = @gateway.authorize(1202, @visacreditcard, @visacredit_options.merge(@visacredit_three_ds_options)) + assert_equal '3DS DECLINED', response.message + assert_equal '65803', response.params['responseCode'] + assert_equal 'not authenticated', response.params['threeDSCheck'] + refute response.success? + assert response.test? + refute response.authorization.blank? + end + + def test_3dsecure2_auth_not_enrolled_card + @visacredit_three_ds_options[:three_d_secure][:enrolled] = 'false' + assert response = @gateway.authorize(1202, @visacreditcard, @visacredit_options.merge(@visacredit_three_ds_options)) + assert_equal '3DS DECLINED', response.message + assert_equal '65803', response.params['responseCode'] + assert_equal 'not checked', response.params['threeDSCheck'] + refute response.success? + assert response.test? + refute response.authorization.blank? + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @visacreditcard, @visacredit_options) diff --git a/test/remote/gateways/remote_checkout_v2_test.rb b/test/remote/gateways/remote_checkout_v2_test.rb index ca05b5c8d19..7361eecea9d 100644 --- a/test/remote/gateways/remote_checkout_v2_test.rb +++ b/test/remote/gateways/remote_checkout_v2_test.rb @@ -2,75 +2,92 @@ class RemoteCheckoutV2Test < Test::Unit::TestCase def setup - @gateway = CheckoutV2Gateway.new(fixtures(:checkout_v2)) + gateway_fixtures = fixtures(:checkout_v2) + gateway_token_fixtures = fixtures(:checkout_v2_token) + @gateway = CheckoutV2Gateway.new(secret_key: gateway_fixtures[:secret_key]) + @gateway_oauth = CheckoutV2Gateway.new({ client_id: gateway_fixtures[:client_id], client_secret: gateway_fixtures[:client_secret] }) + @gateway_token = CheckoutV2Gateway.new(secret_key: gateway_token_fixtures[:secret_key], public_key: gateway_token_fixtures[:public_key]) @amount = 200 - @credit_card = credit_card('4242424242424242', verification_value: '100', month: '6', year: '2025') + @credit_card = credit_card('4242424242424242', verification_value: '100', month: '6', year: Time.now.year + 1) + @credit_card_dnh = credit_card('4024007181869214', verification_value: '100', month: '6', year: Time.now.year + 1) @expired_card = credit_card('4242424242424242', verification_value: '100', month: '6', year: '2010') - @declined_card = credit_card('42424242424242424', verification_value: '234', month: '6', year: '2025') - @threeds_card = credit_card('4485040371536584', verification_value: '100', month: '12', year: '2020') + @declined_card = credit_card('42424242424242424', verification_value: '234', month: '6', year: Time.now.year + 1) + @threeds_card = credit_card('4485040371536584', verification_value: '100', month: '12', year: Time.now.year + 1) + @mada_card = credit_card('5043000000000000', brand: 'mada') - @vts_network_token = network_tokenization_credit_card('4242424242424242', + @vts_network_token = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', - month: '10', - year: '2025', - source: :network_token, - brand: 'visa', - verification_value: nil) - - @mdes_network_token = network_tokenization_credit_card('5436031030606378', - eci: '02', + month: '10', + year: Time.now.year + 1, + source: :network_token, + brand: 'visa', + verification_value: nil + ) + + @mdes_network_token = network_tokenization_credit_card( + '5436031030606378', + eci: '02', payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', - month: '10', - year: '2025', - source: :network_token, - brand: 'master', - verification_value: nil) - - @google_pay_visa_cryptogram_3ds_network_token = network_tokenization_credit_card('4242424242424242', - eci: '05', + month: '10', + year: Time.now.year + 1, + source: :network_token, + brand: 'master', + verification_value: nil + ) + + @google_pay_visa_cryptogram_3ds_network_token = network_tokenization_credit_card( + '4242424242424242', + eci: '05', payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', - month: '10', - year: '2025', - source: :google_pay, - verification_value: nil) + month: '10', + year: Time.now.year + 1, + source: :google_pay, + verification_value: nil + ) - @google_pay_master_cryptogram_3ds_network_token = network_tokenization_credit_card('5436031030606378', + @google_pay_master_cryptogram_3ds_network_token = network_tokenization_credit_card( + '5436031030606378', payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', - month: '10', - year: '2025', - source: :google_pay, - brand: 'master', - verification_value: nil) - - @google_pay_pan_only_network_token = network_tokenization_credit_card('4242424242424242', - month: '10', - year: '2025', - source: :google_pay, - verification_value: nil) - - @apple_pay_network_token = network_tokenization_credit_card('4242424242424242', - eci: '05', + month: '10', + year: Time.now.year + 1, + source: :google_pay, + brand: 'master', + verification_value: nil + ) + + @google_pay_pan_only_network_token = network_tokenization_credit_card( + '4242424242424242', + month: '10', + year: Time.now.year + 1, + source: :google_pay, + verification_value: nil + ) + + @apple_pay_network_token = network_tokenization_credit_card( + '4242424242424242', + eci: '05', payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', - month: '10', - year: '2025', - source: :apple_pay, - verification_value: nil) + month: '10', + year: Time.now.year + 1, + source: :apple_pay, + verification_value: nil + ) @options = { order_id: '1', billing_address: address, + shipping_address: address, description: 'Purchase', - email: 'longbob.longsen@example.com' + email: 'longbob.longsen@example.com', + processing_channel_id: 'pc_lxgl7aqahkzubkundd2l546hdm' } @additional_options = @options.merge( card_on_file: true, transaction_indicator: 2, previous_charge_id: 'pay_123', - processing_channel_id: 'pc_123', - marketplace: { - sub_entity_id: 'ent_123' - } + processing_channel_id: 'pc_123' ) @additional_options_3ds = @options.merge( execute_threed: true, @@ -78,17 +95,67 @@ def setup version: '1.0.2', eci: '06', cavv: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', - xid: 'MDAwMDAwMDAwMDAwMDAwMzIyNzY=' + xid: 'MDAwMDAwMDAwMDAwMDAwMzIyNzY=', + authentication_response_status: 'Y' } ) @additional_options_3ds2 = @options.merge( execute_threed: true, attempt_n3d: true, + challenge_indicator: 'no_preference', + exemption: 'trusted_listing', three_d_secure: { version: '2.0.0', eci: '06', cavv: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', - ds_transaction_id: 'MDAwMDAwMDAwMDAwMDAwMzIyNzY=' + ds_transaction_id: 'MDAwMDAwMDAwMDAwMDAwMzIyNzY=', + authentication_response_status: 'Y' + } + ) + @extra_customer_data = @options.merge( + phone_country_code: '1', + phone: '9108675309' + ) + @payout_options = @options.merge( + source_type: 'currency_account', + source_id: 'ca_spwmped4qmqenai7hcghquqle4', + funds_transfer_type: 'FD', + instruction_purpose: 'leisure', + destination: { + account_holder: { + phone: { + number: '9108675309', + country_code: '1' + }, + identification: { + type: 'passport', + number: '12345788848438' + } + } + }, + currency: 'GBP', + sender: { + type: 'individual', + first_name: 'Jane', + middle_name: 'Middle', + last_name: 'Doe', + address: { + address1: '123 Main St', + address2: 'Apt G', + city: 'Narnia', + state: 'ME', + zip: '12345', + country: 'US' + }, + reference: '012345', + reference_type: 'other', + source_of_funds: 'debit', + identification: { + type: 'passport', + number: 'ABC123', + issuing_country: 'US', + date_of_expiry: '2027-07-07' + } } ) end @@ -104,12 +171,50 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:secret_key], transcript) end + def test_transcript_scrubbing_via_oauth + declined_card = credit_card('4000300011112220', verification_value: '309') + transcript = capture_transcript(@gateway_oauth) do + @gateway_oauth.purchase(@amount, @credit_card, @options) + end + transcript = @gateway_oauth.scrub(transcript) + assert_scrubbed(declined_card.number, transcript) + assert_scrubbed(declined_card.verification_value, transcript) + assert_scrubbed(@gateway_oauth.options[:client_id], transcript) + assert_scrubbed(@gateway_oauth.options[:client_secret], transcript) + end + + def test_network_transaction_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(100, @apple_pay_network_token, @options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@apple_pay_network_token.payment_cryptogram, transcript) + assert_scrubbed(@apple_pay_network_token.number, transcript) + assert_scrubbed(@gateway.options[:secret_key], transcript) + end + + def test_store_transcript_scrubbing + response = nil + transcript = capture_transcript(@gateway) do + response = @gateway_token.store(@credit_card, @options) + end + token = response.responses.first.params['token'] + transcript = @gateway.scrub(transcript) + assert_scrubbed(token, transcript) + end + def test_successful_purchase response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'Succeeded', response.message end + def test_successful_purchase_via_oauth + response = @gateway_oauth.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_vts_network_token response = @gateway.purchase(100, @vts_network_token, @options) assert_success response @@ -117,6 +222,13 @@ def test_successful_purchase_with_vts_network_token assert_not_nil response.params['source']['payment_account_reference'] end + def test_successful_purchase_with_vts_network_token_via_oauth + response = @gateway_oauth.purchase(100, @vts_network_token, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_not_nil response.params['source']['payment_account_reference'] + end + def test_successful_purchase_with_mdes_network_token response = @gateway.purchase(100, @mdes_network_token, @options) assert_success response @@ -130,6 +242,12 @@ def test_successful_purchase_with_google_pay_visa_cryptogram_3ds_network_token assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_google_pay_visa_cryptogram_3ds_network_token_via_oauth + response = @gateway_oauth.purchase(100, @google_pay_visa_cryptogram_3ds_network_token, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_google_pay_master_cryptogram_3ds_network_token response = @gateway.purchase(100, @google_pay_master_cryptogram_3ds_network_token, @options) assert_success response @@ -148,6 +266,19 @@ def test_successful_purchase_with_apple_pay_network_token assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_apple_pay_network_token_via_oauth + response = @gateway_oauth.purchase(100, @apple_pay_network_token, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + # # currently, checkout does not provide any valid test card numbers for testing mada cards + # def test_successful_purchase_with_mada_card + # response = @gateway.purchase(@amount, @mada_card, @options) + # assert_success response + # assert_equal 'Succeeded', response.message + # end + def test_successful_purchase_with_additional_options response = @gateway.purchase(@amount, @credit_card, @additional_options) assert_success response @@ -179,9 +310,51 @@ def test_successful_purchase_with_stored_credentials assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_stored_credentials_via_oauth + initial_options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'recurring' + } + ) + initial_response = @gateway_oauth.purchase(@amount, @credit_card, initial_options) + assert_success initial_response + assert_equal 'Succeeded', initial_response.message + assert_not_nil initial_response.params['id'] + network_transaction_id = initial_response.params['id'] + + stored_options = @options.merge( + stored_credential: { + initial_transaction: false, + reason_type: 'installment', + network_transaction_id: network_transaction_id + } + ) + response = @gateway_oauth.purchase(@amount, @credit_card, stored_options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_stored_credentials_merchant_initiated_transaction_id + stored_options = @options.merge( + stored_credential: { + reason_type: 'installment' + }, + merchant_initiated_transaction_id: 'pay_7emayabnrtjkhkrbohn4m2zyoa321' + ) + response = @gateway.purchase(@amount, @credit_card, stored_options) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_moto_flag response = @gateway.authorize(@amount, @credit_card, @options.merge(transaction_indicator: 3)) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_moto_flag_via_oauth + response = @gateway_oauth.authorize(@amount, @credit_card, @options.merge(transaction_indicator: 3)) assert_success response assert_equal 'Succeeded', response.message end @@ -201,6 +374,14 @@ def test_successful_purchase_includes_avs_result assert_equal 'U.S.-issuing bank does not support AVS.', response.avs_result['message'] end + def test_successful_purchase_includes_avs_result_via_oauth + response = @gateway_oauth.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'G', response.avs_result['code'] + assert_equal 'Non-U.S. issuing bank does not support AVS.', response.avs_result['message'] + end + def test_successful_authorize_includes_avs_result response = @gateway.authorize(@amount, @credit_card, @options) assert_success response @@ -216,6 +397,12 @@ def test_successful_purchase_includes_cvv_result assert_equal 'Y', response.cvv_result['code'] end + def test_successful_purchase_with_extra_customer_data + response = @gateway.purchase(@amount, @credit_card, @extra_customer_data) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_authorize_includes_cvv_result response = @gateway.authorize(@amount, @credit_card, @options) assert_success response @@ -223,6 +410,41 @@ def test_successful_authorize_includes_cvv_result assert_equal 'Y', response.cvv_result['code'] end + def test_successful_authorize_includes_cvv_result_via_oauth + response = @gateway_oauth.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'Y', response.cvv_result['code'] + end + + def test_successful_authorize_with_estimated_type + response = @gateway.authorize(@amount, @credit_card, @options.merge({ authorization_type: 'Estimated' })) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_authorize_with_incremental_authoriation + response = @gateway_oauth.authorize(@amount, @credit_card, @options.merge({ authorization_type: 'Estimated' })) + assert_success response + assert_equal 'Succeeded', response.message + + response = @gateway_oauth.authorize(@amount, @credit_card, @options.merge({ incremental_authorization: response.authorization })) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_authorize_with_estimated_type_via_oauth + response = @gateway_oauth.authorize(@amount, @credit_card, @options.merge({ authorization_type: 'Estimated' })) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_authorize_with_processing_channel_id + response = @gateway.authorize(@amount, @credit_card, @options.merge({ processing_channel_id: 'pc_ovo75iz4hdyudnx6tu74mum3fq' })) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_descriptors options = @options.merge(descriptor_name: 'shop', descriptor_city: 'london') response = @gateway.purchase(@amount, @credit_card, options) @@ -230,6 +452,13 @@ def test_successful_purchase_with_descriptors assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_descriptors_via_oauth + options = @options.merge(descriptor_name: 'shop', descriptor_city: 'london') + response = @gateway_oauth.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_metadata options = @options.merge( metadata: { @@ -242,14 +471,40 @@ def test_successful_purchase_with_metadata assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_metadata_via_oauth + options = @options.merge( + metadata: { + coupon_code: 'NY2018', + partner_id: '123989' + } + ) + response = @gateway_oauth.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_minimal_options response = @gateway.purchase(@amount, @credit_card, billing_address: address) assert_success response assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_shipping_address + response = @gateway.purchase(@amount, @credit_card, shipping_address: address) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_without_phone_number - response = @gateway.purchase(@amount, @credit_card, billing_address: address.update(phone: '')) + response = @gateway.purchase(@amount, @credit_card, billing_address: address.update(phone: nil)) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_without_name + credit_card = credit_card('4242424242424242', verification_value: '100', month: '6', year: Time.now.year + 1, first_name: nil, last_name: nil) + response = @gateway.purchase(@amount, credit_card, @options) + assert_equal response.params['source']['name'], '' assert_success response assert_equal 'Succeeded', response.message end @@ -261,9 +516,15 @@ def test_successful_purchase_with_ip end def test_failed_purchase - response = @gateway.purchase(12305, @credit_card, @options) + response = @gateway.purchase(100, @credit_card_dnh, @options) assert_failure response - assert_equal 'Declined - Do Not Honour', response.message + assert_equal 'Invalid Card Number', response.message + end + + def test_failed_purchase_via_oauth + response = @gateway_oauth.purchase(100, @declined_card, @options) + assert_failure response + assert_equal 'request_invalid: card_number_invalid', response.message end def test_avs_failed_purchase @@ -278,6 +539,12 @@ def test_avs_failed_authorize assert_equal 'request_invalid: card_number_invalid', response.message end + def test_invalid_shipping_address + response = @gateway.authorize(@amount, @credit_card, shipping_address: address.update(country: 'Canada')) + assert_failure response + assert_equal 'request_invalid: country_address_invalid', response.message + end + def test_successful_authorize_and_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -286,6 +553,30 @@ def test_successful_authorize_and_capture assert_success capture end + def test_successful_authorize_and_capture_via_oauth + auth = @gateway_oauth.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway_oauth.capture(nil, auth.authorization) + assert_success capture + end + + def test_successful_authorize_and_partial_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture((@amount / 2).to_i, auth.authorization, { capture_type: 'NonFinal' }) + assert_success capture + end + + def test_successful_authorize_and_partial_capture_via_oauth + auth = @gateway_oauth.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway_oauth.capture((@amount / 2).to_i, auth.authorization, { capture_type: 'NonFinal' }) + assert_success capture + end + def test_successful_authorize_and_capture_with_additional_options auth = @gateway.authorize(@amount, @credit_card, @additional_options) assert_success auth @@ -302,6 +593,14 @@ def test_successful_authorize_and_capture_with_3ds assert_success capture end + def test_successful_authorize_and_capture_with_3ds_via_oauth + auth = @gateway_oauth.authorize(@amount, @credit_card, @additional_options_3ds) + assert_success auth + + assert capture = @gateway_oauth.capture(nil, auth.authorization) + assert_success capture + end + def test_successful_authorize_and_capture_with_3ds2 auth = @gateway.authorize(@amount, @credit_card, @additional_options_3ds2) assert_success auth @@ -310,6 +609,14 @@ def test_successful_authorize_and_capture_with_3ds2 assert_success capture end + def test_successful_authorize_and_capture_with_3ds2_via_oauth + auth = @gateway_oauth.authorize(@amount, @credit_card, @additional_options_3ds2) + assert_success auth + + assert capture = @gateway_oauth.capture(nil, auth.authorization) + assert_success capture + end + def test_successful_authorize_and_capture_with_metadata options = @options.merge( metadata: { @@ -334,9 +641,15 @@ def test_direct_3ds_authorize end def test_failed_authorize - response = @gateway.authorize(12314, @credit_card, @options) + response = @gateway.authorize(12314, @declined_card, @options) assert_failure response - assert_equal 'Invalid Card Number', response.message + assert_equal 'request_invalid: card_number_invalid', response.message + end + + def test_failed_authorize_via_oauth + response = @gateway_oauth.authorize(12314, @declined_card, @options) + assert_failure response + assert_equal 'request_invalid: card_number_invalid', response.message end def test_partial_capture @@ -352,6 +665,128 @@ def test_failed_capture assert_failure response end + def test_failed_capture_via_oauth + response = @gateway_oauth.capture(nil, '') + assert_failure response + end + + def test_successful_credit + @credit_card.first_name = 'John' + @credit_card.last_name = 'Doe' + response = @gateway_oauth.credit(@amount, @credit_card, @options.merge({ source_type: 'currency_account', source_id: 'ca_spwmped4qmqenai7hcghquqle4', account_holder_type: 'individual' })) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_money_transfer_payout_via_credit_individual_account_holder_type + @credit_card.first_name = 'John' + @credit_card.last_name = 'Doe' + response = @gateway_oauth.credit(@amount, @credit_card, @payout_options.merge(account_holder_type: 'individual', payout: true)) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_money_transfer_payout_via_credit_corporate_account_holder_type + @credit_card.name = 'ACME, Inc.' + response = @gateway_oauth.credit(@amount, @credit_card, @payout_options.merge(account_holder_type: 'corporate')) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_money_transfer_payout_reverts_to_credit_if_payout_sent_as_nil + @credit_card.first_name = 'John' + @credit_card.last_name = 'Doe' + response = @gateway_oauth.credit(@amount, @credit_card, @payout_options.merge({ account_holder_type: 'individual', payout: nil })) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_store + response = @gateway_token.store(@credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_unstore_after_store + store = @gateway_token.store(@credit_card, @options) + assert_success store + assert_equal 'Succeeded', store.message + source_id = store.params['id'] + response = @gateway_token.unstore(source_id, @options) + assert_success response + assert_equal response.params['response_code'], '204' + end + + def test_successful_unstore_after_purchase + purchase = @gateway.purchase(@amount, @credit_card, @options) + source_id = purchase.params['source']['id'] + response = @gateway.unstore(source_id, @options) + assert_success response + assert_equal response.params['response_code'], '204' + end + + def test_successful_purchase_after_purchase_with_google_pay + purchase = @gateway.purchase(@amount, @google_pay_master_cryptogram_3ds_network_token, @options) + source_id = purchase.params['source']['id'] + response = @gateway.purchase(@amount, source_id, @options.merge(source_id: source_id, source_type: 'id')) + assert_success response + end + + def test_successful_store_apple_pay + response = @gateway.store(@apple_pay_network_token, @options) + assert_success response + end + + def test_successful_unstore_after_purchase_with_google_pay + purchase = @gateway.purchase(@amount, @google_pay_master_cryptogram_3ds_network_token, @options) + source_id = purchase.params['source']['id'] + response = @gateway.unstore(source_id, @options) + assert_success response + end + + def test_success_store_with_google_pay_3ds + response = @gateway.store(@google_pay_visa_cryptogram_3ds_network_token, @options) + assert_success response + end + + def test_failed_store_oauth_credit_card + response = @gateway_oauth.store(@credit_card, @options) + assert_failure response + assert_equal '401: Unauthorized', response.message + end + + def test_successful_purchase_oauth_after_store_credit_card + store = @gateway_token.store(@credit_card, @options) + assert_success store + token = store.params['id'] + response = @gateway_oauth.purchase(@amount, token, @options) + assert_success response + end + + def test_successful_purchase_after_store_with_google_pay + store = @gateway.store(@google_pay_visa_cryptogram_3ds_network_token, @options) + assert_success store + token = store.params['id'] + response = @gateway.purchase(@amount, token, @options) + assert_success response + end + + def test_successful_purchase_after_store_with_apple_pay + store = @gateway.store(@apple_pay_network_token, @options) + assert_success store + token = store.params['id'] + response = @gateway.purchase(@amount, token, @options) + assert_success response + end + + def test_success_purchase_oauth_after_store_ouath_with_apple_pay + store = @gateway_oauth.store(@apple_pay_network_token, @options) + assert_success store + token = store.params['id'] + response = @gateway_oauth.purchase(@amount, token, @options) + assert_success response + end + def test_successful_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase @@ -362,6 +797,16 @@ def test_successful_refund assert_success refund end + def test_successful_refund_via_oauth + purchase = @gateway_oauth.purchase(@amount, @credit_card, @options) + assert_success purchase + + sleep 1 + + assert refund = @gateway_oauth.refund(@amount, purchase.authorization) + assert_success refund + end + def test_successful_refund_with_metadata options = @options.merge( metadata: { @@ -394,6 +839,11 @@ def test_failed_refund assert_failure response end + def test_failed_refund_via_oauth + response = @gateway_oauth.refund(nil, '') + assert_failure response + end + def test_successful_void auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -402,6 +852,23 @@ def test_successful_void assert_success void end + def test_successful_purchase_store_after_verify + verify = @gateway.verify(@apple_pay_network_token, @options) + assert_success verify + source_id = verify.params['source']['id'] + response = @gateway.purchase(@amount, source_id, @options.merge(source_id: source_id, source_type: 'id')) + assert_success response + assert_success verify + end + + def test_successful_void_via_oauth + auth = @gateway_oauth.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway_oauth.void(auth.authorization) + assert_success void + end + def test_successful_void_with_metadata options = @options.merge( metadata: { @@ -422,6 +889,11 @@ def test_failed_void assert_failure response end + def test_failed_void_via_oauth + response = @gateway_oauth.void('') + assert_failure response + end + def test_successful_verify response = @gateway.verify(@credit_card, @options) # this should only be a Response and not a MultiResponse @@ -433,6 +905,14 @@ def test_successful_verify assert_match %r{Succeeded}, response.message end + def test_successful_verify_via_oauth + response = @gateway_oauth.verify(@credit_card, @options) + assert_instance_of(Response, response) + refute_instance_of(MultiResponse, response) + assert_success response + assert_match %r{Succeeded}, response.message + end + def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response @@ -445,4 +925,10 @@ def test_expired_card_returns_error_code assert_equal 'request_invalid: card_expired', response.message assert_equal 'request_invalid: card_expired', response.error_code end + + def test_successful_purchase_with_idempotency_key + response = @gateway.purchase(@amount, @credit_card, @options.merge(idempotency_key: 'test123')) + assert_success response + assert_equal 'Succeeded', response.message + end end diff --git a/test/remote/gateways/remote_commerce_hub_test.rb b/test/remote/gateways/remote_commerce_hub_test.rb new file mode 100644 index 00000000000..c49ad1ebdaa --- /dev/null +++ b/test/remote/gateways/remote_commerce_hub_test.rb @@ -0,0 +1,291 @@ +require 'test_helper' + +class RemoteCommerceHubTest < Test::Unit::TestCase + def setup + # Uncomment the sleep if you want to run the entire set of remote tests without + # getting 'The transaction limit was exceeded. Please try again!' errors + # sleep 10 + + @gateway = CommerceHubGateway.new(fixtures(:commerce_hub)) + + @amount = 1204 + @credit_card = credit_card('4005550000000019', month: '02', year: '2035', verification_value: '123', first_name: 'John', last_name: 'Doe') + @google_pay = network_tokenization_credit_card( + '4005550000000019', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :google_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @apple_pay = network_tokenization_credit_card( + '4005550000000019', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @declined_apple_pay = network_tokenization_credit_card( + '4000300011112220', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @declined_card = credit_card('4000300011112220', month: '02', year: '2035', verification_value: '123') + @master_card = credit_card('5454545454545454', brand: 'master') + @options = {} + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_whit_physical_goods_indicator + @options[:physical_goods_indicator] = true + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert response.params['transactionDetails']['physicalGoodsIndicator'] + end + + def test_successful_purchase_with_gsf_mit + @options[:data_entry_source] = 'ELECTRONIC_PAYMENT_TERMINAL' + @options[:pos_entry_mode] = 'CONTACTLESS' + response = @gateway.purchase(@amount, @master_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_cit_with_gsf + stored_credential_options = { + initial_transaction: true, + reason_type: 'cardholder', + initiator: 'unscheduled' + } + @options[:eci_indicator] = 'CHANNEL_ENCRYPTED' + @options[:stored_credential] = stored_credential_options + response = @gateway.purchase(@amount, @master_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_failed_avs_cvv_response_codes + @options[:billing_address] = { + address1: '112 Main St.', + city: 'Atlanta', + state: 'GA', + zip: '30301', + country: 'US' + } + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Approved', response.message + assert_equal 'X', response.cvv_result['code'] + assert_equal 'CVV check not supported for card', response.cvv_result['message'] + assert_nil response.avs_result['code'] + end + + def test_successful_purchase_with_billing_and_shipping + response = @gateway.purchase(@amount, @credit_card, @options.merge({ billing_address: address, shipping_address: address })) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_stored_credential_framework + stored_credential_options = { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'merchant' + } + first_response = @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + assert_success first_response + + ntxid = first_response.params['transactionDetails']['retrievalReferenceNumber'] + stored_credential_options = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: ntxid + } + response = @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + assert_success response + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_match 'Unable to assign card to brand: Invalid', response.message + assert_equal '104', response.error_code + end + + def test_successful_authorize + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + # Commenting out until we are able to resolve issue with capture transactions failing at gateway + # def test_successful_authorize_and_capture + # authorize = @gateway.authorize(@amount, @credit_card, @options) + # assert_success authorize + + # capture = @gateway.capture(@amount, authorize.authorization) + # assert_success capture + # end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_match 'Unable to assign card to brand: Invalid', response.message + end + + def test_successful_authorize_and_void + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + + response = @gateway.void(response.authorization, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_failed_void + response = @gateway.void('123', @options) + assert_failure response + assert_equal 'Referenced transaction is invalid or not found', response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'VERIFIED', response.message + end + + def test_successful_verify_with_address + @options[:billing_address] = { + address1: '112 Main St.', + city: 'Atlanta', + state: 'GA', + zip: '30301', + country: 'US' + } + + response = @gateway.verify(@credit_card, @options) + + assert_success response + assert_equal 'VERIFIED', response.message + end + + def test_failed_verify + response = @gateway.verify(@declined_card, @options) + + assert_failure response + end + + def test_successful_purchase_and_refund + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + + response = @gateway.refund(nil, response.authorization, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_and_partial_refund + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + + response = @gateway.refund(@amount - 1, response.authorization, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_failed_refund + response = @gateway.refund(nil, 'abc123|123', @options) + assert_failure response + assert_equal 'Referenced transaction is invalid or not found', response.message + end + + def test_successful_store + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal 'TOKENIZE', response.message + end + + def test_successful_store_with_purchase + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal 'TOKENIZE', response.message + + response = @gateway.purchase(@amount, response.authorization, @options) + assert_success response + end + + def test_successful_purchase_with_google_pay + response = @gateway.purchase(@amount, @google_pay, @options) + assert_success response + assert_equal 'Approved', response.message + assert_equal 'DecryptedWallet', response.params['source']['sourceType'] + end + + def test_successful_purchase_with_apple_pay + response = @gateway.purchase(@amount, @apple_pay, @options) + assert_success response + assert_equal 'Approved', response.message + assert_equal 'DecryptedWallet', response.params['source']['sourceType'] + end + + def test_failed_purchase_with_declined_apple_pay + response = @gateway.purchase(@amount, @declined_apple_pay, @options) + assert_failure response + assert_match 'Unable to assign card to brand: Invalid', response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@gateway.options[:api_key], transcript) + assert_scrubbed(@gateway.options[:api_secret], transcript) + end + + def test_transcript_scrubbing_apple_pay + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @apple_pay, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@apple_pay.number, transcript) + assert_scrubbed(@gateway.options[:api_key], transcript) + assert_scrubbed(@gateway.options[:api_secret], transcript) + assert_scrubbed(@apple_pay.payment_cryptogram, transcript) + end + + def test_successful_purchase_with_encrypted_credit_card + @options[:encryption_data] = { + keyId: '6d0b6b63-3658-4c90-b7a4-bffb8a928288', + encryptionType: 'RSA', + encryptionBlock: 'udJ89RebrHLVxa3ofdyiQ/RrF2Y4xKC/qw4NuV1JYrTDEpNeIq9ZimVffMjgkyKL8dlnB2R73XFtWA4klHrpn6LZrRumYCgoqAkBRJCrk09+pE5km2t2LvKtf/Bj2goYQNFA9WLCCvNGwhofp8bNfm2vfGsBr2BkgL+PH/M4SqyRHz0KGKW/NdQ4Mbdh4hLccFsPjtDnNidkMep0P02PH3Se6hp1f5GLkLTbIvDLPSuLa4eNgzb5/hBBxrq5M5+5n9a1PhQnVT1vPU0WbbWe1SGdGiVCeSYmmX7n+KkVmc1lw0dD7NXBjKmD6aGFAWGU/ls+7JVydedDiuz4E7HSDQ==', + encryptionBlockFields: 'card.cardData:16,card.nameOnCard:10,card.expirationMonth:2,card.expirationYear:4,card.securityCode:3', + encryptionTarget: 'MANUAL' + } + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + end +end diff --git a/test/remote/gateways/remote_credorax_test.rb b/test/remote/gateways/remote_credorax_test.rb index b64d32020f6..30b2dddab3f 100644 --- a/test/remote/gateways/remote_credorax_test.rb +++ b/test/remote/gateways/remote_credorax_test.rb @@ -6,10 +6,10 @@ def setup @amount = 100 @adviser_amount = 1000001 - @credit_card = credit_card('4176661000001015', verification_value: '281', month: '12', year: '2022') - @fully_auth_card = credit_card('5223450000000007', brand: 'mastercard', verification_value: '090', month: '12', year: '2025') - @declined_card = credit_card('4176661000001111', verification_value: '681', month: '12', year: '2022') - @three_ds_card = credit_card('4761739000060016', verification_value: '212', month: '12', year: '2027') + @credit_card = credit_card('4176661000001015', verification_value: '281', month: '12') + @fully_auth_card = credit_card('5223450000000007', brand: 'mastercard', verification_value: '090', month: '12') + @declined_card = credit_card('4176661000001111', verification_value: '681', month: '12') + @three_ds_card = credit_card('4761739000060016', verification_value: '212', month: '12') @options = { order_id: '1', currency: 'EUR', @@ -44,6 +44,67 @@ def setup } } } + + @apple_pay_card = network_tokenization_credit_card( + '4176661000001015', + month: 10, + year: Time.new.year + 2, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + eci: '07', + transaction_id: 'abc123', + source: :apple_pay + ) + + @google_pay_card = network_tokenization_credit_card( + '4176661000001015', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: Time.new.year + 2, + source: :google_pay, + transaction_id: '123456789', + eci: '07' + ) + + @nt_credit_card = network_tokenization_credit_card( + '4176661000001015', + brand: 'visa', + source: :network_token, + payment_cryptogram: 'AgAAAAAAosVKVV7FplLgQRYAAAA=' + ) + end + + def test_successful_purchase_with_apple_pay + response = @gateway.purchase(@amount, @apple_pay_card, @options) + assert_success response + assert_equal '1', response.params['H9'] + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_google_pay + response = @gateway.purchase(@amount, @google_pay_card, @options) + assert_success response + assert_equal '1', response.params['H9'] + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_network_token + response = @gateway.purchase(@amount, @nt_credit_card, @options) + assert_success response + assert_equal '1', response.params['H9'] + assert_equal 'Succeeded', response.message + end + + def test_transcript_scrubbing_network_tokenization_card + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @apple_pay_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@apple_pay_card.number, transcript) + assert_scrubbed(@apple_pay_card.payment_cryptogram, transcript) end def test_invalid_login @@ -413,12 +474,12 @@ def test_failed_referral_cft end def test_successful_credit - response = @gateway.credit(@amount, @credit_card, @options) + response = @gateway.credit(@amount, @credit_card, @options.merge(first_name: 'Test', last_name: 'McTest')) assert_success response assert_equal 'Succeeded', response.message end - def test_failed_credit + def test_failed_credit_with_zero_amount response = @gateway.credit(0, @declined_card, @options) assert_failure response assert_equal 'Invalid amount', response.message @@ -448,7 +509,7 @@ def test_purchase_using_stored_credential_recurring_cit assert_success purchase end - def test_purchase_using_stored_credential_recurring_mit + def test_failed_purchase_using_stored_credential_recurring_mit initial_options = stored_credential_options(:merchant, :recurring, :initial) assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) assert_success purchase @@ -457,6 +518,19 @@ def test_purchase_using_stored_credential_recurring_mit used_options = stored_credential_options(:merchant, :recurring, id: network_transaction_id) assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_failure purchase + assert_match 'Parameter g6 is invalid', purchase.message + end + + def test_successful_purchase_using_stored_credential_recurring_mit + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert_equal '1', purchase.params['A9'] + assert initial_network_transaction_id = purchase.params['Z50'] + + used_options = stored_credential_options(:merchant, :recurring, id: initial_network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) assert_success purchase end @@ -477,7 +551,7 @@ def test_purchase_using_stored_credential_installment_mit assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) assert_success purchase assert_equal '8', purchase.params['A9'] - assert network_transaction_id = purchase.params['Z13'] + assert network_transaction_id = purchase.params['Z50'] used_options = stored_credential_options(:merchant, :installment, id: network_transaction_id) assert purchase = @gateway.purchase(@amount, @credit_card, used_options) @@ -501,7 +575,7 @@ def test_purchase_using_stored_credential_unscheduled_mit assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) assert_success purchase assert_equal '8', purchase.params['A9'] - assert network_transaction_id = purchase.params['Z13'] + assert network_transaction_id = purchase.params['Z50'] used_options = stored_credential_options(:merchant, :unscheduled, id: network_transaction_id) assert purchase = @gateway.purchase(@amount, @credit_card, used_options) diff --git a/test/remote/gateways/remote_cyber_source_rest_test.rb b/test/remote/gateways/remote_cyber_source_rest_test.rb new file mode 100644 index 00000000000..bca3e7499e7 --- /dev/null +++ b/test/remote/gateways/remote_cyber_source_rest_test.rb @@ -0,0 +1,515 @@ +require 'test_helper' + +class RemoteCyberSourceRestTest < Test::Unit::TestCase + def setup + @gateway = CyberSourceRestGateway.new(fixtures(:cybersource_rest)) + @amount = 10221 + @card_without_funds = credit_card('42423482938483873') + @bank_account = check(account_number: '4100', routing_number: '121042882') + @declined_bank_account = check(account_number: '550111', routing_number: '121107882') + + @visa_card = credit_card('4111111111111111', verification_value: '987', month: 12, year: 2031) + + @master_card = credit_card('2222420000001113', brand: 'master') + @discover_card = credit_card('6011111111111117', brand: 'discover') + + @apple_pay = network_tokenization_credit_card( + '4111111111111111', + payment_cryptogram: 'AceY+igABPs3jdwNaDg3MAACAAA=', + month: '11', + year: Time.now.year + 1, + source: :apple_pay, + verification_value: 569 + ) + + @google_pay = network_tokenization_credit_card( + '4111111111111111', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: Time.now.year + 1, + source: :google_pay, + verification_value: 569 + ) + + @google_pay_master = network_tokenization_credit_card( + '5555555555554444', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: Time.now.year + 1, + source: :google_pay, + verification_value: 569, + brand: 'master' + ) + + @apple_pay_jcb = network_tokenization_credit_card( + '3566111111111113', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: Time.now.year + 1, + source: :apple_pay, + verification_value: 569, + brand: 'jcb' + ) + + @apple_pay_american_express = network_tokenization_credit_card( + '378282246310005', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: Time.now.year + 1, + source: :apple_pay, + verification_value: 569, + brand: 'american_express' + ) + + @google_pay_discover = network_tokenization_credit_card( + '6011111111111117', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: Time.now.year + 1, + source: :google_pay, + verification_value: 569, + brand: 'discover' + ) + + @billing_address = { + name: 'John Doe', + address1: '1 Market St', + city: 'san francisco', + state: 'CA', + zip: '94105', + country: 'US', + phone: '4158880000' + } + + @options = { + order_id: generate_unique_id, + currency: 'USD', + email: 'test@cybs.com', + billing_address: { + name: 'John Doe', + address1: '1 Market St', + city: 'san francisco', + state: 'CA', + zip: '94105', + country: 'US', + phone: '4158880000' + } + } + end + + def test_handle_credentials_error + gateway = CyberSourceRestGateway.new({ merchant_id: 'abc123', public_key: 'abc456', private_key: 'def789' }) + response = gateway.authorize(@amount, @visa_card, @options) + + assert_equal('Authentication Failed', response.message) + end + + def test_successful_authorize + response = @gateway.authorize(@amount, @visa_card, @options) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + end + + def test_successful_authorize_with_billing_address + @options[:billing_address] = @billing_address + response = @gateway.authorize(@amount, @visa_card, @options) + + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + end + + def test_failure_authorize_with_declined_credit_card + response = @gateway.authorize(@amount, @card_without_funds, @options) + + assert_failure response + assert_match %r{Invalid account}, response.message + assert_equal 'INVALID_ACCOUNT', response.error_code + end + + def test_successful_capture + authorize = @gateway.authorize(@amount, @visa_card, @options) + response = @gateway.capture(@amount, authorize.authorization, @options) + + assert_success response + assert_equal 'PENDING', response.message + end + + def test_successful_capture_with_partial_amount + authorize = @gateway.authorize(@amount, @visa_card, @options) + response = @gateway.capture(@amount - 10, authorize.authorization, @options) + + assert_success response + assert_equal 'PENDING', response.message + end + + def test_failure_capture_with_higher_amount + authorize = @gateway.authorize(@amount, @visa_card, @options) + response = @gateway.capture(@amount + 10, authorize.authorization, @options) + + assert_failure response + assert_match(/exceeds/, response.params['message']) + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @visa_card, @options) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + end + + def test_successful_purchase_with_credit_card_ignore_avs + @options[:ignore_avs] = 'true' + response = @gateway.purchase(@amount, @visa_card, @options) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + end + + def test_successful_purchase_with_network_token_ignore_avs + @options[:ignore_avs] = 'true' + response = @gateway.purchase(@amount, @apple_pay, @options) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + end + + def test_successful_purchase_with_credit_card_ignore_cvv + @options[:ignore_cvv] = 'true' + response = @gateway.purchase(@amount, @visa_card, @options) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + end + + def test_successful_purchase_with_network_token_ignore_cvv + @options[:ignore_cvv] = 'true' + response = @gateway.purchase(@amount, @apple_pay, @options) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + end + + def test_successful_refund + purchase = @gateway.purchase(@amount, @visa_card, @options) + response = @gateway.refund(@amount, purchase.authorization, @options) + + assert_success response + assert response.test? + assert_equal 'PENDING', response.message + assert response.params['id'].present? + assert response.params['_links']['void'].present? + end + + def test_failure_refund + purchase = @gateway.purchase(@amount, @card_without_funds, @options) + response = @gateway.refund(@amount, purchase.authorization, @options) + + assert_failure response + assert response.test? + assert_match %r{Declined - One or more fields in the request contains invalid data}, response.params['message'] + assert_equal 'INVALID_DATA', response.params['reason'] + end + + def test_successful_partial_refund + purchase = @gateway.purchase(@amount, @visa_card, @options) + response = @gateway.refund(@amount / 2, purchase.authorization, @options) + + assert_success response + assert response.test? + assert_equal 'PENDING', response.message + assert response.params['id'].present? + assert response.params['_links']['void'].present? + end + + def test_successful_repeat_refund_transaction + purchase = @gateway.purchase(@amount, @visa_card, @options) + response1 = @gateway.refund(@amount, purchase.authorization, @options) + + assert_success response1 + assert response1.test? + assert_equal 'PENDING', response1.message + assert response1.params['id'].present? + assert response1.params['_links']['void'] + + response2 = @gateway.refund(@amount, purchase.authorization, @options) + assert_success response2 + assert response2.test? + assert_equal 'PENDING', response2.message + assert response2.params['id'].present? + assert response2.params['_links']['void'] + + assert_not_equal response1.params['_links']['void'], response2.params['_links']['void'] + end + + def test_successful_credit + response = @gateway.credit(@amount, @visa_card, @options) + + assert_success response + assert response.test? + assert_equal 'PENDING', response.message + assert response.params['id'].present? + assert_nil response.params['_links']['capture'] + end + + def test_failure_credit + response = @gateway.credit(@amount, @card_without_funds, @options) + + assert_failure response + assert response.test? + assert_match %r{Decline - Invalid account number}, response.message + assert_equal 'INVALID_ACCOUNT', response.error_code + end + + def test_successful_void + authorize = @gateway.authorize(@amount, @visa_card, @options) + response = @gateway.void(authorize.authorization, @options) + assert_success response + assert response.params['id'].present? + assert_equal 'REVERSED', response.message + assert_nil response.params['_links']['capture'] + end + + def test_failure_void_using_card_without_funds + authorize = @gateway.authorize(@amount, @card_without_funds, @options) + response = @gateway.void(authorize.authorization, @options) + assert_failure response + assert_match %r{Declined - The request is missing one or more fields}, response.params['message'] + assert_equal 'INVALID_REQUEST', response.params['status'] + end + + def test_successful_verify + response = @gateway.verify(@visa_card, @options) + assert_success response + assert response.params['id'].present? + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + end + + def test_failure_verify + response = @gateway.verify(@card_without_funds, @options) + assert_failure response + assert_match %r{Decline - Invalid account number}, response.message + assert_equal 'INVALID_ACCOUNT', response.error_code + end + + def test_successful_authorize_with_apple_pay + response = @gateway.authorize(@amount, @apple_pay, @options) + + assert_success response + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + end + + def test_successful_authorize_with_google_pay + response = @gateway.authorize(@amount, @apple_pay, @options) + + assert_success response + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + end + + def test_successful_purchase_with_apple_pay_jcb + response = @gateway.purchase(@amount, @apple_pay_jcb, @options) + + assert_success response + assert_equal 'AUTHORIZED', response.message + end + + def test_successful_purchase_with_apple_pay_american_express + response = @gateway.purchase(@amount, @apple_pay_american_express, @options) + + assert_success response + assert_equal 'AUTHORIZED', response.message + end + + def test_successful_purchase_with_google_pay_master + response = @gateway.purchase(@amount, @google_pay_master, @options) + + assert_success response + assert_equal 'AUTHORIZED', response.message + end + + def test_successful_authorize_with_google_pay_discover + response = @gateway.purchase(@amount, @google_pay_discover, @options) + + assert_success response + assert_equal 'AUTHORIZED', response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.authorize(@amount, @visa_card, @options) + end + + transcript = @gateway.scrub(transcript) + assert_scrubbed(@visa_card.number, transcript) + assert_scrubbed(@visa_card.verification_value, transcript) + end + + def test_transcript_scrubbing_bank + @options[:billing_address] = @billing_address + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @bank_account, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@bank_account.account_number, transcript) + assert_scrubbed(@bank_account.routing_number, transcript) + end + + def test_successful_authorize_with_bank_account + @options[:billing_address] = @billing_address + response = @gateway.authorize(@amount, @bank_account, @options) + assert_success response + assert_equal 'PENDING', response.message + end + + def test_successful_purchase_with_bank_account + @options[:billing_address] = @billing_address + response = @gateway.purchase(@amount, @bank_account, @options) + assert_success response + assert_equal 'PENDING', response.message + end + + def test_failed_authorize_with_bank_account + @options[:billing_address] = @billing_address + response = @gateway.authorize(@amount, @declined_bank_account, @options) + assert_failure response + assert_equal 'Decline - General decline by the processor.', response.message + end + + def test_failed_authorize_with_bank_account_missing_country_code + response = @gateway.authorize(@amount, @bank_account, @options.except(:billing_address)) + assert_failure response + assert_equal 'Declined - The request is missing one or more fields', response.params['message'] + end + + def stored_credential_options(*args, ntid: nil) + @options.merge(stored_credential: stored_credential(*args, network_transaction_id: ntid)) + end + + def test_purchase_using_stored_credential_initial_mit + options = stored_credential_options(:merchant, :internet, :initial) + options[:reason_code] = '4' + assert auth = @gateway.authorize(@amount, @visa_card, options) + assert_success auth + assert purchase = @gateway.purchase(@amount, @visa_card, options) + assert_success purchase + end + + def test_purchase_using_stored_credential_recurring_cit + options = stored_credential_options(:cardholder, :recurring, :initial) + options[:reason_code] = '4' + assert auth = @gateway.authorize(@amount, @visa_card, options) + assert_success auth + used_store_credentials = stored_credential_options(:cardholder, :recurring, ntid: auth.network_transaction_id) + used_store_credentials[:reason_code] = '4' + assert purchase = @gateway.purchase(@amount, @visa_card, used_store_credentials) + assert_success purchase + end + + def test_purchase_using_stored_credential_recurring_mit + options = stored_credential_options(:merchant, :recurring, :initial) + options[:reason_code] = '4' + assert auth = @gateway.authorize(@amount, @visa_card, options) + assert_success auth + used_store_credentials = stored_credential_options(:merchant, :recurring, ntid: auth.network_transaction_id) + used_store_credentials[:reason_code] = '4' + assert purchase = @gateway.purchase(@amount, @visa_card, used_store_credentials) + assert_success purchase + end + + def test_purchase_using_stored_credential_installment_cit + options = stored_credential_options(:cardholder, :installment, :initial) + options[:reason_code] = '4' + assert auth = @gateway.authorize(@amount, @visa_card, options) + assert_success auth + used_store_credentials = stored_credential_options(:cardholder, :installment, ntid: auth.network_transaction_id) + used_store_credentials[:reason_code] = '4' + assert purchase = @gateway.purchase(@amount, @visa_card, used_store_credentials) + assert_success purchase + end + + def test_purchase_using_stored_credential_installment_mit + options = stored_credential_options(:merchant, :installment, :initial) + options[:reason_code] = '4' + assert auth = @gateway.authorize(@amount, @visa_card, options) + assert_success auth + used_store_credentials = stored_credential_options(:merchant, :installment, ntid: auth.network_transaction_id) + used_store_credentials[:reason_code] = '4' + assert purchase = @gateway.purchase(@amount, @visa_card, used_store_credentials) + assert_success purchase + end + + def test_failure_stored_credential_invalid_reason_code + options = stored_credential_options(:cardholder, :internet, :initial) + assert auth = @gateway.authorize(@amount, @master_card, options) + assert_equal(auth.params['status'], 'INVALID_REQUEST') + assert_equal(auth.params['message'], 'Declined - One or more fields in the request contains invalid data') + assert_equal(auth.params['details'].first['field'], 'processingInformation.authorizationOptions.initiator.merchantInitiatedTransaction.reason') + end + + def test_auth_and_purchase_with_network_txn_id + options = stored_credential_options(:merchant, :recurring, :initial) + options[:reason_code] = '4' + assert auth = @gateway.authorize(@amount, @visa_card, options) + assert purchase = @gateway.purchase(@amount, @visa_card, options.merge(network_transaction_id: auth.network_transaction_id)) + assert_success purchase + end + + def test_successful_purchase_with_reconciliation_id + options = @options.merge(reconciliation_id: '1936831') + assert response = @gateway.purchase(@amount, @visa_card, options) + assert_success response + end + + def test_successful_authorization_with_reconciliation_id + options = @options.merge(reconciliation_id: '1936831') + assert response = @gateway.authorize(@amount, @visa_card, options) + assert_success response + assert !response.authorization.blank? + end + + def test_successful_verify_zero_amount + @options[:zero_amount_auth] = true + response = @gateway.verify(@visa_card, @options) + assert_success response + assert_match '0.00', response.params['orderInformation']['amountDetails']['authorizedAmount'] + assert_equal 'AUTHORIZED', response.message + end + + def test_successful_bank_account_purchase_with_sec_code + options = @options.merge(sec_code: 'WEB') + response = @gateway.purchase(@amount, @bank_account, options) + assert_success response + assert_equal 'PENDING', response.message + end + + def test_successful_purchase_with_solution_id + ActiveMerchant::Billing::CyberSourceRestGateway.application_id = 'A1000000' + assert response = @gateway.purchase(@amount, @visa_card, @options) + assert_success response + assert !response.authorization.blank? + ensure + ActiveMerchant::Billing::CyberSourceGateway.application_id = nil + end + + def test_successful_purchase_in_australian_dollars + @options[:currency] = 'AUD' + response = @gateway.purchase(@amount, @visa_card, @options) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + assert_equal 'AUD', response.params['orderInformation']['amountDetails']['currency'] + end +end diff --git a/test/remote/gateways/remote_cyber_source_test.rb b/test/remote/gateways/remote_cyber_source_test.rb index 1434a3e05be..7f235ad910a 100644 --- a/test/remote/gateways/remote_cyber_source_test.rb +++ b/test/remote/gateways/remote_cyber_source_test.rb @@ -10,37 +10,61 @@ def setup @credit_card = credit_card('4111111111111111', verification_value: '987') @declined_card = credit_card('801111111111111') - @master_credit_card = credit_card('5555555555554444', + @master_credit_card = credit_card( + '5555555555554444', verification_value: '321', month: '12', year: (Time.now.year + 2).to_s, - brand: :master) + brand: :master + ) @pinless_debit_card = credit_card('4002269999999999') - @elo_credit_card = credit_card('5067310000000010', + @elo_credit_card = credit_card( + '5067310000000010', verification_value: '321', month: '12', year: (Time.now.year + 2).to_s, - brand: :elo) - @three_ds_unenrolled_card = credit_card('4000000000000051', + brand: :elo + ) + @three_ds_unenrolled_card = credit_card( + '4000000000000051', verification_value: '321', month: '12', year: (Time.now.year + 2).to_s, - brand: :visa) - @three_ds_enrolled_card = credit_card('4000000000000002', + brand: :visa + ) + @three_ds_enrolled_card = credit_card( + '4000000000000002', verification_value: '321', month: '12', year: (Time.now.year + 2).to_s, - brand: :visa) - @three_ds_invalid_card = credit_card('4000000000000010', + brand: :visa + ) + @three_ds_invalid_card = credit_card( + '4000000000000010', verification_value: '321', month: '12', year: (Time.now.year + 2).to_s, - brand: :visa) - @three_ds_enrolled_mastercard = credit_card('5200000000001005', + brand: :visa + ) + @three_ds_enrolled_mastercard = credit_card( + '5200000000001005', verification_value: '321', month: '12', year: (Time.now.year + 2).to_s, - brand: :master) + brand: :master + ) + @visa_network_token = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @amex_network_token = network_tokenization_credit_card( + '378282246310005', + brand: 'american_express', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) @amount = 100 @@ -62,7 +86,20 @@ def setup ignore_cvv: 'true', commerce_indicator: 'internet', user_po: 'ABC123', - taxable: true + taxable: true, + sales_slip_number: '456', + airline_agent_code: '7Q', + tax_management_indicator: 1, + invoice_amount: '3', + original_amount: '4', + reference_data_code: 'ABC123', + invoice_number: '123', + mobile_remote_payment_type: 'A1', + vat_tax_rate: '1' + } + + @capture_options = { + gratuity_amount: '3.50' } @subscription_options = { @@ -93,18 +130,13 @@ def test_transcript_scrubbing end def test_network_tokenization_transcript_scrubbing - credit_card = network_tokenization_credit_card('4111111111111111', - brand: 'visa', - eci: '05', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') - transcript = capture_transcript(@gateway) do - @gateway.authorize(@amount, credit_card, @options) + @gateway.authorize(@amount, @visa_network_token, @options) end transcript = @gateway.scrub(transcript) - assert_scrubbed(credit_card.number, transcript) - assert_scrubbed(credit_card.payment_cryptogram, transcript) + assert_scrubbed(@visa_network_token.number, transcript) + assert_scrubbed(@visa_network_token.payment_cryptogram, transcript) assert_scrubbed(@gateway.options[:password], transcript) end @@ -209,7 +241,22 @@ def test_successful_authorization_with_elo end def test_successful_authorization_with_installment_data - options = @options.merge(installment_total_count: 5, installment_plan_type: 1, first_installment_date: '300101') + options = @options.merge( + installment_total_count: 2, + installment_total_amount: 0.50, + installment_plan_type: 1, + first_installment_date: '300101', + installment_annual_interest_rate: 1.09, + installment_grace_period_duration: 1 + ) + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_authorization_with_less_installment_data + options = @options.merge(installment_grace_period_duration: '1') + assert response = @gateway.authorize(@amount, @credit_card, options) assert_successful_response(response) assert !response.authorization.blank? @@ -233,6 +280,19 @@ def test_successful_authorization_with_airline_agent_code assert_successful_response(response) end + def test_successful_authorization_with_tax_mgmt_indicator + options = @options.merge(tax_management_indicator: '3') + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_successful_response(response) + end + + def test_successful_bank_account_purchase_with_sec_code + options = @options.merge(sec_code: 'WEB') + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + assert response = @gateway.purchase(@amount, bank_account, options) + assert_successful_response(response) + end + def test_unsuccessful_authorization assert response = @gateway.authorize(@amount, @declined_card, @options) assert response.test? @@ -325,6 +385,48 @@ def test_successful_purchase assert_successful_response(response) end + def test_successful_purchase_with_bank_account + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + assert response = @gateway.purchase(10000, bank_account, @options) + assert_successful_response(response) + end + + # To properly run this test couple of test your account needs to be enabled to + # handle canadian bank accounts. + def test_successful_purchase_with_a_canadian_bank_account_full_number + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + @options[:currency] = 'CAD' + assert response = @gateway.purchase(10000, bank_account, @options) + assert_successful_response(response) + end + + def test_successful_purchase_with_a_canadian_bank_account_8_digit_number + bank_account = check({ account_number: '4100', routing_number: '11000015' }) + @options[:currency] = 'CAD' + assert response = @gateway.purchase(10000, bank_account, @options) + assert_successful_response(response) + end + + def test_successful_purchase_with_bank_account_savings_account + bank_account = check({ account_number: '4100', routing_number: '011000015', account_type: 'savings' }) + assert response = @gateway.purchase(10000, bank_account, @options) + assert_successful_response(response) + end + + def test_unsuccessful_purchase_with_bank_account_card_declined + bank_account = check({ account_number: '4201', routing_number: '011000015' }) + assert response = @gateway.purchase(10000, bank_account, @options) + assert_failure response + assert_equal 'General decline by the processor', response.message + end + + def test_unsuccessful_purchase_with_bank_account_merchant_configuration + bank_account = check({ account_number: '4241', routing_number: '011000015' }) + assert response = @gateway.purchase(10000, bank_account, @options) + assert_failure response + assert_equal 'A problem exists with your CyberSource merchant configuration', response.message + end + def test_successful_purchase_with_national_tax_indicator assert purchase = @gateway.purchase(@amount, @credit_card, @options.merge(national_tax_indicator: 1)) assert_successful_response(purchase) @@ -494,7 +596,7 @@ def test_authorize_and_capture assert auth = @gateway.authorize(@amount, @credit_card, @options) assert_successful_response(auth) - assert capture = @gateway.capture(@amount, auth.authorization) + assert capture = @gateway.capture(@amount, auth.authorization, @capture_options) assert_successful_response(capture) end @@ -574,11 +676,41 @@ def test_successful_refund_with_solution_id ActiveMerchant::Billing::CyberSourceGateway.application_id = nil end + def test_successful_refund_with_bank_account_follow_on + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + assert response = @gateway.purchase(10000, bank_account, @options) + assert_successful_response(response) + + assert response = @gateway.refund(10000, response.authorization, @options) + assert_successful_response(response) + end + def test_network_tokenization_authorize_and_capture - credit_card = network_tokenization_credit_card('4111111111111111', - brand: 'visa', + assert auth = @gateway.authorize(@amount, @visa_network_token, @options) + assert_successful_response(auth) + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_successful_response(capture) + end + + def test_network_tokenization_with_amex_cc_and_basic_cryptogram + assert auth = @gateway.authorize(@amount, @amex_network_token, @options) + assert_successful_response(auth) + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_successful_response(capture) + end + + def test_network_tokenization_with_amex_cc_longer_cryptogram + # Generate a random 40 bytes binary amex cryptogram => Base64.encode64(Random.bytes(40)) + long_cryptogram = "NZwc40C4eTDWHVDXPekFaKkNYGk26w+GYDZmU50cATbjqOpNxR/eYA==\n" + + credit_card = network_tokenization_credit_card( + '378282246310005', + brand: 'american_express', eci: '05', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + payment_cryptogram: long_cryptogram + ) assert auth = @gateway.authorize(@amount, credit_card, @options) assert_successful_response(auth) @@ -587,6 +719,43 @@ def test_network_tokenization_authorize_and_capture assert_successful_response(capture) end + def test_purchase_with_network_tokenization_with_amex_cc + assert auth = @gateway.purchase(@amount, @amex_network_token, @options) + assert_successful_response(auth) + end + + def test_purchase_with_apple_pay_network_tokenization_visa_subsequent_auth + credit_card = network_tokenization_credit_card('4111111111111111', + brand: 'visa', + eci: '05', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '016150703802094' + } + + assert auth = @gateway.purchase(@amount, credit_card, @options) + assert_successful_response(auth) + end + + def test_purchase_with_apple_pay_network_tokenization_mastercard_subsequent_auth + credit_card = network_tokenization_credit_card('5555555555554444', + brand: 'master', + eci: '05', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '0602MCC603474' + } + + assert auth = @gateway.purchase(@amount, credit_card, @options) + assert_successful_response(auth) + end + def test_successful_authorize_with_mdd_fields (1..20).each { |e| @options["mdd_field_#{e}".to_sym] = "value #{e}" } @@ -609,6 +778,8 @@ def test_successful_capture_with_mdd_fields assert_successful_response(capture) end + # this test should probably be removed, the fields do not appear to be part of the + # most current XSD file, also they are not added to the request correctly as top level fields def test_merchant_description merchant_options = { merchantInformation: { @@ -639,6 +810,11 @@ def test_successful_authorize_with_nonfractional_currency assert_successful_response(response) end + def test_successful_authorize_with_additional_purchase_totals_data + assert response = @gateway.authorize(100, @credit_card, @options.merge(discount_management_indicator: 'T', purchase_tax_amount: 7.89)) + assert_successful_response(response) + end + def test_successful_subscription_authorization assert response = @gateway.store(@credit_card, @subscription_options) assert_successful_response(response) @@ -648,6 +824,16 @@ def test_successful_subscription_authorization assert !response.authorization.blank? end + def test_successful_subscription_authorization_with_bank_account + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + assert response = @gateway.store(bank_account, order_id: generate_unique_id) + assert_successful_response(response) + + assert response = @gateway.purchase(@amount, response.authorization, order_id: generate_unique_id) + assert_successful_response(response) + assert !response.authorization.blank? + end + def test_successful_subscription_purchase assert response = @gateway.store(@credit_card, @subscription_options) assert_successful_response(response) @@ -712,9 +898,24 @@ def test_successful_standalone_credit_to_subscription_with_merchant_descriptor assert_successful_response(response) end + def test_successful_credit_with_bank_account + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + assert response = @gateway.credit(10000, bank_account, order_id: generate_unique_id) + + assert_successful_response(response) + end + def test_successful_create_subscription assert response = @gateway.store(@credit_card, @subscription_options) assert_successful_response(response) + assert_equal 'credit_card', response.authorization.split(';')[7] + end + + def test_successful_create_subscription_with_bank_account + bank_account = check({ account_number: '4100', routing_number: '011000015' }) + assert response = @gateway.store(bank_account, @subscription_options) + assert_successful_response(response) + assert_equal 'check', response.authorization.split(';')[7] end def test_successful_create_subscription_with_elo @@ -747,8 +948,11 @@ def test_successful_update_subscription_billing_address assert response = @gateway.store(@credit_card, @subscription_options) assert_successful_response(response) - assert response = @gateway.update(response.authorization, nil, - { order_id: generate_unique_id, setup_fee: 100, billing_address: address, email: 'someguy1232@fakeemail.net' }) + assert response = @gateway.update( + response.authorization, + nil, + { order_id: generate_unique_id, setup_fee: 100, billing_address: address, email: 'someguy1232@fakeemail.net' } + ) assert_successful_response(response) end @@ -813,6 +1017,18 @@ def test_successful_3ds_requests_with_unenrolled_card # Extract this XML and generate an accessToken. Using this access token to create a form, visit the stepUpURL provided # and check the network exchange in the browser dev console for a CCA, which will contain a usable PaRes. Documentation for this feature # can be found at https://docs.cybersource.com/content/dam/new-documentation/documentation/en/fraud-management/payer-auth/so/payer-auth-so.pdf + # Version => September 2017 + # Chapter 2 "Authenticating Enrolled Cards" page 27 + # something like: + # + # + #
+ # + # + # + #
+ # + # def test_successful_3ds_validate_purchase_request assert response = @gateway.purchase(1202, @three_ds_enrolled_card, @options.merge(payer_auth_validate_service: true, pares: pares)) assert_equal '100', response.params['reasonCode'] @@ -974,6 +1190,72 @@ def test_successful_subsequent_unscheduled_cof_purchase assert_successful_response(response) end + def test_successful_authorize_with_3ds_exemption + @options[:three_d_secure] = { + version: '2.0', + eci: '05', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC' + } + + assert response = @gateway.authorize(@amount, @three_ds_enrolled_card, @options.merge(three_ds_exemption_type: 'moto')) + assert_successful_response(response) + end + + def test_successful_purchase_with_3ds_exemption + @options[:three_d_secure] = { + version: '2.0', + eci: '05', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC' + } + + assert response = @gateway.purchase(@amount, @three_ds_enrolled_card, @options.merge(three_ds_exemption_type: 'moto')) + assert_successful_response(response) + end + + def test_successful_recurring_cof_authorize_with_3ds_exemption + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: '' + } + + @options[:three_d_secure] = { + version: '2.0', + eci: '05', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC' + } + + assert response = @gateway.authorize(@amount, @three_ds_enrolled_card, @options.merge(three_ds_exemption_type: CyberSourceGateway::THREEDS_EXEMPTIONS[:stored_credential])) + assert_successful_response(response) + end + + def test_successful_recurring_cof_purchase_with_3ds_exemption + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: '' + } + + @options[:three_d_secure] = { + version: '2.0', + eci: '05', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC' + } + + assert response = @gateway.purchase(@amount, @three_ds_enrolled_card, @options.merge(three_ds_exemption_type: CyberSourceGateway::THREEDS_EXEMPTIONS[:stored_credential])) + assert_successful_response(response) + end + def test_invalid_field @options = @options.merge({ address: { @@ -992,7 +1274,7 @@ def test_invalid_field def pares <<~PARES - eNrdWdmvqsjWf9+J/8NJ30e7m0FRuPHspBgFBGWS4Y15EAEZBPnrb+k+U/c9nXR/Lzf5dmJ2Ua5atebfWrgzszaOWSMOhzZ+3ylx1/lp/CmPPv+yNY8rxewMUUvHQXJ69bBGf3nfnYAedy+C12o7xuLNDLOYiQdvJP3Tcq7KsmLWUvcZUt/jtsvr6h37Hf0d3yFfH+FFbZj5Vf++88MbLarv6w2FY5sd8uVxd41bkX2P4sQfyn6HfDzukO/nTsNz1UGhpzx6VzH9pKCYFWKqZNnRKrIaN+IIRWXB5x3ypNhFfh+/4yi6RTc48Qlb/xvF/o2SO+S1v2ue7MC1HiBvDEehrD/u7KB52rgKH+/kGt0h35528dTUVQwp4IFv6x3yXbjGr97RP/49ecPdnem87/r8+qNQ1FMobLtDXvu7rvf7oXt3d8iX1S707/d3AAANbL68auC5LHTb9jiOT/l5CZV9keziMH9HCSgU/P86Bcq0bvM+uz5F/ePGDnmKgrz8+b4z8rSCl7Xxp+laVtCLWd83/0aQcRx/H1e/122KQIFRBKUQSBB1efqvXz5OxZFYJfU/Osb4VV3loV/ms9/DyFDiPqujT99k+xkbU39ywhCdY36DrH4LsXX123MHXWEE5In8nOkPmv2dW/4sbNv5v3WZjz0v+BOj950eJ/EzIuJPli5+/uVffyc12DyNu/7/IspXMX7k8JXf2S+H+F06l5ixanw9vl3vAob4nN2u7Yopo/Xnr+c+KHfIN9m/KPbhxR+s9UF41w00OVrjyc66mpMmd70K+7E1dbxWSAOp0CajcXPgh6QtN9ht6O5Kd7mfRYv245P76JmJRJPtnl68Pfq602YRSQ1xPxwrmkkGSUVBd1FMZtDzdUzUknjFZt9cjrOnR/K2Eb1c2S59gvYVHqPx9sYsj9zijVg5bWjzWzk6TNuQ2ESMWRtZJIb945HMZb2SPbXb2kSEHS61tepCOkVS0mjn4GTygmGk5EDVmyuxeBPOidRL69ksqHuBPVYX87EF5HEsLFFhPQLp2DbyGU5pIpe7bLWxE4ZLo2fCeeRJY/I8YhXUa3G5Xbw1G2h0fFPb4SEYJWnSKxpnMRHMN2x1uWM9pjoUYJ1VL5dopkjG/oi4xvj58w9R9cUzcvz48IRDoBTr9/7HyhiCIg571YcVgvlsGb9+MszPxyyvf/10+KzA+lO3v346fmb8Nsorv2TqKyyeMDKZum3q9pUQ8HvrG8EnwEAejPp9g42v9adXKLQ75M9XvmRg4rbPE5hisHQqosirM8PQ9DIFo0iDVNSU060YmpNzPg4TPpK5WdRZUgCVTi+37JIL1IjSQOt4wNKkonUjo7ns4u2saQI3Smdr5kqFUQSAWRyTKaGG8w9PKAfXntgAx3rPkQrPoNlgJY3wk0VCeQ8KLlXo9evM4o2ZFMfCz0XkSE20v6SuTVxETr0HDt35Nj+4uDVJLMjpVD3TQDEFVM3Cq94EV77TcKoP7PMD0qSLN8jlEV3L132aCWJ+RB+KycGPNiosgB/af+0Vr72HMluEapa+IgqyoqOjMLos1Atqx9I66zrSxbeJLGBozrOxO7Rc4+FEGRbcWaG/aDyOyneNx1SzVFMxrFH84CQ/OU3fOT21srEyvKrlU8Owou/hlUd9mxoUjRxZ7XVqzwJP9WwCDVbixJrg8NR88QadpdAcVfs2OTEzkD7s4ZqgPJvQr7T4Xz7jeACODNBI8PyeSWW45sDiDb0dMM5mlJxAQ9M2HF0yEu06P1C7vWx48XD1eJRe9q5zyE8OoHLrAqwjkLlMBXk1OPrQ96g53/aLt1nK0+1Sq6cj5xGTIPTHgujTdbpqOHvDql3QnZabreXk/taeZGZmEkK0CZjcvKOSZW450UF+MF25eJNEvmi3yyYITDRUFWTrpSqtyjoXNDd92y7B7X7bOrcUt7dtOGtYKm6NIBD7uRDEImWrVJMmxOKsxdvmOnj2xse7UpajmtKme3Ogb/f5Zpb2gJjAD5a3W15uHFGqt+pap8zZkEPlBE4o8zC8YXicpYDCYaExrxohJEkgDsxGOFL3tN72F+d8ABr66C9YQ0XSUiEpM07IseuXLiMMCjKKLNAADSsM3bAMA3wwwigA0LM6agJtj9BAHAELjk//7TWSBgnJwbhmaEi51150JU27Iw9cUR5dmtasPcxVgWE6YfEGNIunRxh/NPqMwIhNNZumjQ219mWMceZE8O9g32DIwdLPbAtqEySvm3SOE1hgp9lHZC/efh7bdPRBTXLngrYUXk4tmT5ml6mO9vp4zMl7KPTNwVYfAUPMsY1li7fI0evDFca2QRXumRzVWZxUQVu7pjYptjUeWYCp5gXmW5Z7LIeqtpd7BXioD0pxHb0MVzprXcZh8eaupE5hw5euH5pyNM10o2yCik7Tlk45ntbCEbhu4O91NGTr+2FFt76B3d2VOh4qlQhsHoMyCSmsHFSv0OTLTuKouQo0Mb9np6C9YeXorXVUXwKw7NZjk2wz+6gwqMBcGcEAgrDkLJoDCqxP+xlEdKam2SWls3vGAx1An4jP1EPvrVLwqfusMiulUMaj5squ6InADda6xmm0prUKc4F5B2wY4wIvWgVdGByKm/sLEeHcw1/pRWCdMXdWu4gvXWj1TJvPN7cQx5P5kyoMM/oZBRxgI39/aLFOHM7k+UEkQ2phclmKlF0KYaieMWQbrY3T8QLWPUoJW5PEY1xIlmo0OJWYIqTeFIu3UdRJKBw/cPphOq3L25zilNle4vMSNfbMss8dU5cu6lWLQc8cL6d57cxWegbFcMVskbXTyM9UzVm8eRbNxuJ6i9i6OaN8otBlwrRsGj7IMQjmeAP0pejIj0eGtEck3SR3murmkaLQpiCtw2rKboMub2DN5By7yuYmjJTJ5nUpXnln8k6Xp2BjIqfqbpw9Nck2Nhx3hIkCqyok+bLVRb0QIPyTFosGfFrZOgrbF6bK9AG94UHDDljA5dU1u2GSKQnnrbq5YvkKC7FTWt5KuuaSSJ8rVyCoARDgCfJ/Rs+fwSmXmjDJ8es3OI205Ql3Tug9qzwkW8vOSGgn9qdwSvyv4FT8UhRecMr9JZxeqXvEfIPSSZkBrhTWH6H0tfcDlO7H/wKnxduPkMhNfxMQDVgO09eZAzdFpmerMK3Da4nGBp0FVy19ShsKUwM1LDyTuyiM+LIBmJTgL+xmwv+4b6sQcOB99D+BzY7NgtGkA2/oqTidFXG90bd+DWFiUvoQix/WCWMwM57ON7/ppYmklVlaW0paZOGhzBlemVR+qgkevfU9HdeaJAvlhjPtzUVcBbp6ERdvwUWZlXJSxkRMkLQ47KeL2AXx0rcPDHj49ZbCtak6Iy1j2KdJc/OsnFuMO0/JJshDfFDcKZQACksyKGkhEpKeOhgEWmj2gLvHzqSwmG6EJHcPfe7JnNL5Q5yRTaJ7ZxdEqpta6+NmKI9RsXZW2BTJMIWco5lKpwxlDOFqskgCW/feqaqbwHPxIVUBBa5HGxu1s9gRS3xJilz1SGvVW6n3+LI+YRU5lhjpw+I+9+dsDwaSGPvBCR+HLTrwN0HkuMQ85D/AJmAZGuK08QFEygsgGRYIT/AE6bOBUcD6A/wgPCAjdA7MPbqg26/Q5bF0rghM6tzoMCubvwddd+/qwQwhShfn7j+NH+uPEfojDfTdV6qzdA8goGkGUYTXcZTTF6TrNKOMoq/BsohOwgy8r/nHlR6M/9L07Wgwz/Rh8WYY6ONgaqjKgg8w1tYcD1vL8MQPQ2s7dJXt9f0asJu5YEpk4N31uB9ftxQ0nY58DawN3cFCigaRXxRYfCWw+A6TYy3Ya369fjaaklx7YnYPVRjbB1oDbJrCLkQ2R5c0Bvze0IpSduUU4LZ2khZvmRXFlcitmocsaAhQXVKEFtaz4/Y+gDNG+xtFEJv9Uldhospmmuenop4qwVweCFNW1Jub3ZbXDsa4GJT7IBCqiTkP0UqasTChcnOzje/BPExDFd2vtXSdV5ywmamtMWgtoQcZY3Vrcpusr2PTDylxb6DvltoUFKHBW1vWu5/y++WQ0I+KkpI0doVamNhyv99LOXiUqWoS8p5z2usQ2lq8N1ztkBwnI2EIl+BhBkOFLrOblyM+BVdu3crexLjjFFLx7F2vp+S6tpepJCvA5iLxeCsyX2BRjcLujnOIz6HjFNVcx8ribRu3mKw/SLI+0p3U0Lds86AthvMxLmwD8W+CCrspIKj08jdQOfhsgY+EvdpXaciruV+t5Hz+fwoq83+DyuV/BSrZl/7xCSruX9jNCHAKfcIJlOkfAMqDNsdrZ0qYtU+kfRO3Kr3fLOf7IJMBtPh8FwAQxaS469pD4sSYkMExzkwRsI8pxSfLvfE2ZoJbu1wTSEN7h9MJ72LYBHr9LLaXBklAWcMZLcSdW2nZ43K8HwYh5uW+P/cGW4mISDQXWkEvyWmPhSE4FJvYpHqKH7fnq3cmEi+8DYfK2neyoJq3xRsGgprr4STuNewMchIluBlxlbzaIkKIyXJSHnlkRZoW1UxRerIqlLKvQxBHoSj2BrHNLmYrpQGEArNJWt0R7oQaDMvgkbAy2JuXOlIYWwYjaytKP0mPaBXMrEneOKxk7NmaSvqiauzmqFty4tQhHkEYx06YwWl9MttlglPSCemYm4W6Wf0VUPD1GLB/CSb0VyiBM9ofwOQ5b3xMTZBP3NDjKBQcnCDcr9FwehbrZ6lWWG19MLleMRXsOZVB8P02l5m1MLZ6j92Op/MmqM4bEgZf6gWpUxQ/7+1frcY54IprBFuLGtdPW9vbt4V1K8+q16krBA3p6TLWbNQhy4RSUnnAHxzZbFy8a+j5iKs+mUeSxDrgmJ+2DXZDwjPUjh3dVWscZWNjatQNSzEMIp0P9PkSmY0WDAYb33yrOabUwMFZAZNKd+ba8CJ0zFKZG3GbBFxzvy/ecI2L49IrbHlQL3CmlDbMXImRfGCXV3GFngZtLubb/Xy7SkNR+14iy0KyqntWbIOLSdaNgVsscYPxpJ9aTnFWlerP+TlSBL/Abl5Jmc76dFGH5sqmTHdxHtC8FgKnLCp3nESZD3T+uIsOTnR6n6A9IRwXb5VwJpb2GW277ORjamXx7Uid65hrBZSYMZK63UiDyq/2aJJr7ae9PfL9zR3y7W3e9/d8r98zXj+4PF/B//hDzH8AlPBTsQ== + eNrdWVnPqsjWvn8T/8NOn0u7m0FROHG/STGIiKBMMtwxDwIyg/z6U/ruqfvsTrq/m5N8JsSiWLVqzU8t2OlJE4asFvp9E77vpLBt3Tj8lAaff0knQEp22WzrUXB886EJAfrL++4C1LB9EbxG2zEUat1PQibsnZF0L8u5zPOSWR/bz5B6CJs2vZfv2O/o7/gO+XoLN2r8xC27953r17Qgv683FI5tdsiX210RNgL7HoSR2+fdDvm43SHf113656iFQk9p8O5aCX3mMMcrhZVvBUezkK3wKh8dFnzeIU+KXeB24TuOolt0gxOfsPW/UezfKLlDXvO76skOFPce8sZwFMr648wOmqcJS//xTq7RHfLtbhdO1b0MIQVc8G28Q74LV7nlO/rH35M3nN3p1vuuS4sfhaKeQmHbHfKa37Wd2/Xtu71Dvox2vjsM7wAAGpj7vFDAc5ippulw3D7ez0uo7ItkF/rpO0pAoeD/axXI43uTdknxFPWPEzvkKQry8uf7TkvjEm7WhJ+mIi+hF5Ouq/6NIOM4/j6ufr83MQIFRhGUQiBB0Kbxv375WBUGQhnd/9Eyxi3vZeq7eTq7HYwMKeySe/Dpm2w/Y6OrT04YonLMb5DVbz62Ln97zqArjIA8kZ8z/UGzv7PLn4VtWve3NnGx5wZ/YvS+U8MofEZE+MlQhc+//OvvpAabxmHb/V9E+SrGjxy+8ru6eR++O8vlPccN0gpO/UnVr3L7SOdTx6+T+PPXdR+UO+Sb7F8U+/DiD9b6ILxg+32EFIdS56aldnloNGKZmq0POF6Q4jbs4jr3qkNnlXVs0IrDhM35cTpktiPcEAXdHNzYdq1TsHg7iCKgm7GKK0TFEH7i+UHFSEzMuvzUAwrV/VJADWQvlIziN4FCZse7mxTsw3HcUEiCOdLrjLeKxZvWrBQHD720kEJvo03HuDbpRpgBitVgsyLliA6t85ashcNqGZnS2d2qocv7EW1MUZOtqvhsrhn1sHgzm2FDUzw6HqyYrUStVsw4bSlBJgQ6omV/W0lzWib5MEAbpNHgPI4N1Z9TNKGVodHi2XcEyiGhTHO+tyQNazFKU7O8J9mDpd+waKscLsoSDE3S+PUGf+CV2/bNzZXw6dwH4PPnH6Lqi2fE8PHhCYtAKdbt3I+R1ntZ6HeyCysE89nQfv2k6Z/PSXr/9dPpswTrz7359dP5M+M2QVq6OXMvYPGEkcncm+revBICPje+EXwCDOTByN8n2LC4f3qFQrND/rzlSwbo3C6NYIrB0ikJwl6eGYamlzEYBRrEgiJd6qyvLtb13E/4SKZ6dk+iDMh0fKuTW8pTI0oDpd0DliYlpR0ZxWYXb1dF4bnxeDVmLpcYiQeYwTGJ5Cv4/uHweW+bE+vhWOdYx8zRaNZbHUd4JQGfD17GxRK9fq1ZvDGTZBn4NQusYxUcbrFtEjeBkwfPolvX3Pc2bkxHFqR0LF9pIOk8Kid+oVZesW8VnOo88/qANPHiDXJ5BEX+2k/RQbgf0Yekc/BSRokF8KLd11z2mntIs0HIeu5KAi9KKjryo81CvaB2LK2ytnW8uSaReAzNOSY2QMtVDk7kfsZdJfqLxuMofdd4jBVD1iXNGIUPTuKT0/Sd01MrE8v9Qs6fGvolPfjFHnVNqpcUcmSV16oDCxzZMQnUWwkTq4PTU/PFG3SWRHPU3TXJiZnB8cMetg7yqw79Sgv/5TNuD8CZAQoJns+ZWIRjDize0PqEcSYjpQTq66ZmqUctUor5gZrNbbMXToWzR+llZ1un9GIBKjVuwDgDkUtkkJa9pfZdh+pzDVNxPqbxdqncpzPnELA4dOeM6OJ1vKo4c8PKrddelputYaXu1pxEZmYiQjCJVevvLZnMUwPWPPHBtPni7Sjss2a7rDxPR31ZQrZOLNOyqHJeVavbZgnqod5adYyb28afFSwWtprnCd2c8UIWs2WsHCfE4IzF26boHXPj4m0uisGdUqahOtH1MNd6bvaIDlxvWddpvrGE430rr1VKnzXRly7ggjIPzen7x/XoUTixeNMLheCjyBN6ZsOfqSG+b7ubdT0BBX10N6yiguNSIik9jMix7ZY2w/cSMgosUAB9Xwt0xTIMcMEIowBAz6qoDpQDQgNhBCw4P/13UEgaRCQH45qhIeVBedHlNG2Pe2AL4mjTtGIcYK7yDNPyizegGHt6hPFHo88IDNhYMWla21BrV8QYa454dwCHCkNOhnplG3DXQfTaSeU4ngVmnHxE9uLt57FNBx/UJHfNaEPai7Eh0ufkNt2DgzqeU3Lw+a46mfLDY4g5NLFk8RZY6v1UwNjWqMy+kqM8C5PMK2tbVybJNMYzCzBZv8F8S1KH5VDZdFInAw/5QUm2peb+SmWN29gv3uzVsZVY/6Xrh6YcTTPtKOqgpOO4oWNuTyv+CGzbcw8q6rP34bSiG1fDBnslj6dSJjxzj0GZ+BhWDqqTaPJlJ2FUbAmaeH9gJ6+psXx01iqqLgFYtuuxiraJeZYYlGcKhtcAzy85g+aABOvTYQYBnchxcovpZEj2QAXQJ8Iz9dChkbJ9bD+rzErKpPGs2KItOAKwvbWqcAqtKI3E3GDeARPGOL8XjIzONA7F9cONCHDu4a7UzDOumD3LbbDPbWj1RJmvtZ0J40X/SRWGGf2MAg6wgXs4NVgr9Ffy+iCiPjYwMc8Fysx535evGLIN1trlfAPrDqX4rU7iIc5HSznorVKIEVKtssXbKKgkFG7fc+ppuqzzeo5xSm9u4XWJagdm2aWWrh5vcqGEoGPOt8u8tmYjvoKsLzBTYM04cBNZsRZvjkGzobDeIqaqz+g+kug8Yho29h/k6HlzuAHqUrDExyNBmjMSb6KBptp5pCi0ykjjtJqSulfFDayZnGWWyVz5gTSZe/UYrpwrOdD5xdvoyKUctKsjR8nGbDWBnyiwKn1ynzeqoGY86xCkwaLePi5NFV28PZgyUXu0xr2K7TGPS8siqbGjfuSvW3lTYOkK87FLnNc5feeiQJ1LmyeoHhAvkP8zev4MTrlYh0mOF9/gNFCWF9y6oENSOkiyFq2RUC7sT+GU+F/BqfClKLzglPtLOC2oIWC+QekkzQCXMuOPUPqa+wFKD+N/gdPi7UdI5Ka/CYgaLIfxa82JmwLdMWWY1n6Ro6FGJ16hxE9pfX6qoIaZo3M3iRFeNgCT5P2F3XT4j7umDAEH7kf/E9hs2cQbddpz+o4K41kS1ht1694hTExS52Phw7hgDKaH07V2q+44kbQ0H9eGFGeJf8pTZi9N8n66E3u07jo6vCtHkc83nG5ubsLKU+WbsHjzbtIs5ZM0RkKExNnpMN2E1guXrnliwMO9bylcmcor0jCaeZkUO03yucG46xRtvNTHe8me/CNAYUkGOc0HfNRRJ41AM8Xscfvc6hQW0hUfpfapSx2Rk1q3DxOyilTnaoNAtmNjfd70+TnI1tYKmwIRppB11uPjJUEZjS90Fol8YgP7hrLm91x4imVAgeJsYqNyFVpiiS9JgSsf8V12VvIQ3tYXrCTHHCNdWNzn7pocQE8SY9db/uO0Rft9zQscF+mn9AfYBCxDQ5zWPoBIegEkwwL+CZ4gfh5gJLD+AD8ID8gInQNzj87o5it0OSydSjwTWzXtJ3n196BrcAoHZgiR2zg3/DR+jD9G6I800Hdfqa7HwYOApmhE5hfjKMYvSFdpRhoFV4FlEZ34GThf84/LHRj/ue6aQa9f6RPslDT0cdIVVGbBBxgra24Pj5b+Zd/3jWnRZXJQD2vAbuaMyZF+b6/Hw/jaJaPpeNzfgbGhW1hIUS9wswwLCwILB5gca95c79fr50HzKN4dIRl8Gcb2iVYAG8fwFCLqo01qPT5UtCTlbT55uKlcjou3xAjCUuBW1UPkFQTINilAC6vJeTv04IrR7kbiheqwVGWYqKIep+klu08lry9PhC5Kcm0n9bJoYYwLXn7wPL6cmGsfrI4z5kdUqm+24eDN/dSXwVDcj8W84vjNTG21XmkI1UsYo12T22hdjFXXx8RQQd8tlcnLfG1vbFlnuKTD7RTRj5I6RnFo83d+YvPD4XBMwSOPZZ0QD5zVFL1vKuFBs5VTdJ60iCFsYg8zGCp0m+00H/HJK7h1IzoTY4+TT4WzUxSXqFiby/goSsDkAuFcZ4nLs6hCYYNlncKrb1lZOd9DafG2DRtMVB8keT/T7bGi62TzoA2GczHObzzhb4IKu8kgqHTiN1A5uWyGj4S5OpSxv5dTt1yJ6fz/FFTm/waV2/8KVJIv58cnqNh/YTfNwyn0CSdQpn8AKA9aH4tWP2LGIToeqrCR6cNmOQ+9SHrQ4vPAAyAIUTaoyuPICSEhgnOY6AJgH1OMT4Zd701MB3WzXBNIRTunywVvQ3gIdLpZaG4VEoH8Dns0H7fq3DDH5Ticej7ci1137TS2FBCBqG60hN6iywHzfXDKNqFOddR+3F4L50pEjl/3p9I4tCIv6/XiDQPenetgJ+5U7AxSEiW4GbGltNwivI+JYpSf98iK1A2qmoL4YpQoZRa9Fwa+IHQasU1uenOMPQgFehU1qsUPhOz1S+8RsSI46Ld7IDGmCEbWlKRuOj6ClTezOllzWM6YszHl9E1W2M1ZNcTIuvt4AGEcu2Aap3TRbOYRTh0vSMvUBmon96+Agq9Hj/1LMKG/Qgns0f4AJs9+46NrgnzCih5HPuNgB2F/jYbLs1g/S7XEKuuTznWSLmHPrgyC77e+TL/zY6N2WH2+XDdeed2QMPhix4utLPv52f511Lh6XFYE8Ghxx9XL1nQOTWbU+VV2WnmFoD493cY7G7TIMqKkWOzxB0dWGxtvK3o+47JLpsHxyFrgnF62FVYj/hVqx472qtHOorbRFarGYgyDSOcCdb4FeqV4vcaGtWtU55jqOdgrYMfcnrnGv/Ets5TmSthGHlcNw+INV7gwzJ3MFHv5BnvK44aZSyEQT+yyEFbopVfmbK6Ha10c++zuOpEo8tHq3rFC49108l5puMESNYwn9dJwkrUqZXdOr4HEuxlWOzmlW+vLTe6rgo2Z9mY9oHkNBHZZVGpZkTSf6PQxCBZOtGoXoR3BnxdvJX8lluYVbdrk4mJyaeybkbreQ67hUWLGSKquSY1KC3PUybXy07M98v3NHfLtbd7393yv7xmvDy7PV/A/foj5D/LlVqY= PARES end diff --git a/test/remote/gateways/remote_d_local_test.rb b/test/remote/gateways/remote_d_local_test.rb index 10d707a111f..89b516dd9d3 100644 --- a/test/remote/gateways/remote_d_local_test.rb +++ b/test/remote/gateways/remote_d_local_test.rb @@ -50,6 +50,39 @@ def test_successful_purchase assert_match 'The payment was paid', response.message end + def test_successful_purchase_with_save_option + response = @gateway.purchase(@amount, @credit_card, @options.merge(save: true)) + assert_success response + assert_equal true, response.params['card']['save'] + assert_equal 'CREDIT', response.params['card']['type'] + assert_not_empty response.params['card']['card_id'] + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_with_network_tokens + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_with_network_tokens_and_store_credential_type + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + response = @gateway.purchase(@amount, credit_card, @options.merge!(stored_credential_type: 'SUBSCRIPTION')) + assert_success response + assert_match 'SUBSCRIPTION', response.params['card']['stored_credential_type'] + assert_match 'The payment was paid', response.message + end + + def test_successful_purchase_with_network_tokens_and_store_credential_usage + options = @options.merge!(stored_credential: stored_credential(:merchant, :recurring, ntid: 'abc123')) + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + response = @gateway.purchase(@amount, credit_card, options) + assert_success response + assert_match 'USED', response.params['card']['stored_credential_usage'] + assert_match 'The payment was paid', response.message + end + def test_successful_purchase_with_installments response = @gateway.purchase(@amount, @credit_card, @options_argentina_installments) assert_success response @@ -68,6 +101,39 @@ def test_successful_purchase_cabal assert_match 'The payment was paid', response.message end + def test_successful_inquire_with_payment_id + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_match 'The payment was paid', response.message + + authorization = response.params['id'] + response = @gateway.inquire(authorization, @options) + assert_success response + assert_match 'PAID', response.params['status'] + assert_match 'The payment was paid.', response.params['status_detail'] + end + + def test_successful_inquire_with_order_id + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_match 'The payment was paid', response.message + + purchase_payment_id = response.params['id'] + order_id = response.params['order_id'] + + response = @gateway.inquire(nil, { order_id: order_id }) + check_payment_id = response.params['payment_id'] + assert_success response + assert_match purchase_payment_id, check_payment_id + end + + def test_successful_purchase_with_original_order_id + response = @gateway.purchase(@amount, @credit_card, @options.merge(original_order_id: '123ABC')) + assert_success response + assert_match 'The payment was paid', response.message + assert_match '123ABC', response.params['original_order_id'] + end + def test_successful_purchase_with_more_options options = @options.merge( order_id: '1', @@ -139,6 +205,16 @@ def test_failed_purchase assert_match 'The payment was rejected', response.message end + def test_failed_purchase_with_network_tokens + credit_card = network_tokenization_credit_card( + '4242424242424242', + payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=' + ) + response = @gateway.purchase(@amount, credit_card, @options.merge(description: '300')) + assert_failure response + assert_match 'The payment was rejected', response.message + end + def test_failed_document_format response = @gateway.purchase(@amount, @credit_card, @options.merge(document: 'bad_document')) assert_failure response diff --git a/test/remote/gateways/remote_decidir_test.rb b/test/remote/gateways/remote_decidir_test.rb index f270f891db5..22831af2ed7 100644 --- a/test/remote/gateways/remote_decidir_test.rb +++ b/test/remote/gateways/remote_decidir_test.rb @@ -30,6 +30,16 @@ def setup amount: 1500 } ] + @network_token = network_tokenization_credit_card( + '4012001037141112', + brand: 'visa', + eci: '05', + payment_cryptogram: '000203016912340000000FA08400317500000000', + name: 'Tesest payway' + ) + + @failed_message = ['PEDIR AUTORIZACION | request_authorization_card', 'COMERCIO INVALIDO | invalid_card'] + @failed_code = ['1, call_issuer', '3, config_error'] end def test_successful_purchase @@ -53,6 +63,22 @@ def test_successful_purchase_with_amex assert response.authorization end + def test_successful_purchase_with_network_token + options = { + card_holder_door_number: 1234, + card_holder_birthday: '200988', + card_holder_identification_type: 'DNI', + card_holder_identification_number: '44444444', + order_id: SecureRandom.uuid, + last_4: @credit_card.last_digits + } + response = @gateway_for_purchase.purchase(500, @network_token, options) + + assert_success response + assert_equal 'approved', response.message + assert response.authorization + end + # This test is currently failing. # Decidir hasn't been able to provide a valid Diners Club test card number. # @@ -169,9 +195,14 @@ def test_failed_purchase_with_bad_csmdds def test_failed_purchase response = @gateway_for_purchase.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'COMERCIO INVALIDO | invalid_card', response.message - assert_equal '3, config_error', response.error_code - assert_match Gateway::STANDARD_ERROR_CODE[:config_error], response.error_code + assert_equal @failed_message.include?(response.message), true + assert_equal @failed_code.include?(response.error_code), true + + if response.error_code.start_with?('1') + assert_match Gateway::STANDARD_ERROR_CODE[:call_issuer], response.error_code + else + assert_match Gateway::STANDARD_ERROR_CODE[:config_error], response.error_code + end end def test_failed_purchase_with_invalid_field @@ -196,8 +227,13 @@ def test_successful_authorize_and_capture def test_failed_authorize response = @gateway_for_auth.authorize(@amount, @declined_card, @options) assert_failure response - assert_equal 'PEDIR AUTORIZACION | request_authorization_card', response.message - assert_match '1, call_issuer', response.error_code + assert_equal @failed_message.include?(response.message), true + assert_equal @failed_code.include?(response.error_code), true + if response.error_code.start_with?('1') + assert_match Gateway::STANDARD_ERROR_CODE[:call_issuer], response.error_code + else + assert_match Gateway::STANDARD_ERROR_CODE[:config_error], response.error_code + end end def test_failed_partial_capture @@ -274,6 +310,18 @@ def test_failed_verify assert_match %r{PEDIR AUTORIZACION | request_authorization_card}, response.message end + def test_successful_inquire + response = @gateway_for_purchase.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'approved', response.message + assert response.authorization + + response = @gateway_for_purchase.inquire(response.params['id']) + assert_success response + assert_equal 'approved', response.message + assert response.authorization + end + def test_invalid_login_without_api_key gateway = DecidirGateway.new(api_key: '') diff --git a/test/remote/gateways/remote_deepstack_test.rb b/test/remote/gateways/remote_deepstack_test.rb new file mode 100644 index 00000000000..f34a26960c2 --- /dev/null +++ b/test/remote/gateways/remote_deepstack_test.rb @@ -0,0 +1,230 @@ +require 'test_helper' + +class RemoteDeepstackTest < Test::Unit::TestCase + def setup + Base.mode = :test + @gateway = DeepstackGateway.new(fixtures(:deepstack)) + + @credit_card = credit_card + @amount = 100 + + @credit_card = ActiveMerchant::Billing::CreditCard.new( + number: '4111111111111111', + verification_value: '999', + month: '01', + year: '2029', + first_name: 'Bob', + last_name: 'Bobby' + ) + + @invalid_card = ActiveMerchant::Billing::CreditCard.new( + number: '5146315000000051', + verification_value: '999', + month: '01', + year: '2029', + first_name: 'Failure', + last_name: 'Fail' + ) + + address = { + address1: '123 Some st', + address2: '', + first_name: 'Bob', + last_name: 'Bobberson', + city: 'Some City', + state: 'CA', + zip: '12345', + country: 'USA', + phone: '1231231234', + email: 'test@test.com' + } + + shipping_address = { + address1: '321 Some st', + address2: '#9', + first_name: 'Jane', + last_name: 'Doe', + city: 'Other City', + state: 'CA', + zip: '12345', + country: 'USA', + phone: '1231231234', + email: 'test@test.com' + } + + @options = { + order_id: '1', + billing_address: address, + shipping_address: shipping_address, + description: 'Store Purchase' + } + end + + def test_successful_token + response = @gateway.get_token(@credit_card, @options) + assert_success response + + sale = @gateway.purchase(@amount, response.authorization, @options) + assert_success sale + assert_equal 'Approved', sale.message + end + + def test_failed_token + response = @gateway.get_token(@invalid_card, @options) + assert_failure response + assert_equal 'InvalidRequestException: Card number is invalid.', response.message + end + + # Feature currently gated. Will be released in future version + # def test_successful_vault + + # response = @gateway.gettoken(@credit_card, @options) + # assert_success response + + # vault = @gateway.store(response.authorization, @options) + # assert_success vault + + # sale = @gateway.purchase(@amount, vault.authorization, @options) + # assert_success sale + + # end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_true response.params['captured'] + end + + def test_successful_purchase_with_more_options + additional_options = { + ip: '127.0.0.1', + email: 'joe@example.com' + } + + sent_options = @options.merge(additional_options) + + response = @gateway.purchase(@amount, @credit_card, sent_options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @invalid_card, @options) + assert_failure response + assert_not_equal 'Approved', response.message + end + + def test_successful_authorize + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert_equal 'Approved', auth.message + end + + def test_successful_authorize_and_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal 'Approved', capture.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @invalid_card, @options) + assert_failure response + assert_not_equal 'Approved', response.message + end + + def test_partial_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount - 1, auth.authorization) + assert_success capture + end + + def test_failed_capture + response = @gateway.capture(@amount, '') + assert_failure response + assert_equal 'Current transaction does not exist or is in an invalid state.', response.message + end + + # This test will always void because we determine void/refund based on settlement status of the charge request (i.e can't refund a transaction that was just created) + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal 'Approved', refund.message + end + + # This test will always void because we determine void/refund based on settlement status of the charge request (i.e can't refund a transaction that was just created) + def test_partial_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount - 1, purchase.params['id']) + assert_success refund + assert_equal @amount - 1, refund.params['amount'] + end + + # This test always be a void because we determine void/refund based on settlement status of the charge request (i.e can't refund a transaction that was just created) + def test_failed_refund + response = @gateway.refund(@amount, '') + assert_failure response + assert_equal 'Specified transaction does not exist.', response.message + end + + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(0, auth.params['id']) + assert_success void + assert_equal 'Approved', void.message + end + + def test_failed_void + response = @gateway.void(0, '') + assert_failure response + assert_equal 'Specified transaction does not exist.', response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match %r{Approved}, response.message + end + + def test_failed_verify + response = @gateway.verify(@invalid_card, @options) + assert_failure response + assert_match %r{Invalid Request: Card number is invalid.}, response.message + end + + def test_invalid_login + gateway = DeepstackGateway.new(publishable_api_key: '', app_id: '', shared_secret: '', sandbox: true) + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match 'Specified transaction does not exist', response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + expiration = '%02d%02d' % [@credit_card.month, @credit_card.year % 100] + assert_scrubbed(expiration, transcript) + + transcript = capture_transcript(@gateway) do + @gateway.get_token(@credit_card, @options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed('pk_test_XQS71KYAW9HW7XQOGAJIY4ENHZYZEO0C', transcript) + end +end diff --git a/test/remote/gateways/remote_ebanx_test.rb b/test/remote/gateways/remote_ebanx_test.rb index 3d2197042af..266c7b4e2ed 100644 --- a/test/remote/gateways/remote_ebanx_test.rb +++ b/test/remote/gateways/remote_ebanx_test.rb @@ -22,8 +22,14 @@ def setup metadata: { metadata_1: 'test', metadata_2: 'test2' - } + }, + tags: EbanxGateway::TAGS, + soft_descriptor: 'ActiveMerchant', + email: 'neymar@test.com' } + + @hiper_card = credit_card('6062825624254001') + @elo_card = credit_card('6362970000457013') end def test_successful_purchase @@ -32,6 +38,24 @@ def test_successful_purchase assert_equal 'Accepted', response.message end + def test_successful_purchase_hipercard + response = @gateway.purchase(@amount, @hiper_card, @options) + assert_success response + assert_equal 'Accepted', response.message + end + + def test_successful_purchase_elocard + response = @gateway.purchase(@amount, @elo_card, @options) + assert_success response + assert_equal 'Accepted', response.message + end + + def test_successful_store_elocard + response = @gateway.purchase(@amount, @elo_card, @options) + assert_success response + assert_equal 'Accepted', response.message + end + def test_successful_purchase_with_more_options options = @options.merge({ order_id: generate_unique_id, @@ -110,6 +134,13 @@ def test_failed_authorize assert_equal 'NOK', response.error_code end + def test_failed_authorize_no_email + response = @gateway.authorize(@amount, @declined_card, @options.except(:email)) + assert_failure response + assert_equal 'Field payment.email is required', response.message + assert_equal 'BP-DR-15', response.error_code + end + def test_successful_partial_capture_when_include_capture_amount_is_not_passed auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -192,6 +223,23 @@ def test_successful_store_and_purchase_as_brazil_business store = @gateway.store(@credit_card, options) assert_success store + assert_equal store.authorization.split('|')[1], 'visa' + + assert purchase = @gateway.purchase(@amount, store.authorization, options) + assert_success purchase + assert_equal 'Accepted', purchase.message + end + + def test_successful_store_and_purchase_as_brazil_business_with_hipercard + options = @options.update(document: '32593371000110', + person_type: 'business', + responsible_name: 'Business Person', + responsible_document: '32593371000111', + responsible_birth_date: '1/11/1975') + + store = @gateway.store(@hiper_card, options) + assert_success store + assert_equal store.authorization.split('|')[1], 'hipercard' assert purchase = @gateway.purchase(@amount, store.authorization, options) assert_success purchase @@ -254,9 +302,20 @@ def test_successful_verify_for_mexico end def test_failed_verify - response = @gateway.verify(@declined_card, @options) + declined_card = credit_card('6011088896715918') + response = @gateway.verify(declined_card, @options) assert_failure response - assert_match %r{Invalid card or card type}, response.message + assert_match %r{Not accepted}, response.message + end + + def test_successful_inquire + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + inquire = @gateway.inquire(purchase.authorization) + assert_success inquire + + assert_equal 'Accepted', purchase.message end def test_invalid_login diff --git a/test/remote/gateways/remote_element_test.rb b/test/remote/gateways/remote_element_test.rb index 93a0911de77..649e014bb81 100644 --- a/test/remote/gateways/remote_element_test.rb +++ b/test/remote/gateways/remote_element_test.rb @@ -12,6 +12,32 @@ def setup billing_address: address, description: 'Store Purchase' } + + @google_pay_network_token = network_tokenization_credit_card( + '4444333322221111', + month: '01', + year: Time.new.year + 2, + first_name: 'Jane', + last_name: 'Doe', + verification_value: '888', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '05', + transaction_id: '123456789', + source: :google_pay + ) + + @apple_pay_network_token = network_tokenization_credit_card( + '4895370015293175', + month: '10', + year: Time.new.year + 2, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + payment_cryptogram: 'CeABBJQ1AgAAAAAgJDUCAAAAAAA=', + eci: '07', + transaction_id: 'abc123', + source: :apple_pay + ) end def test_successful_purchase @@ -50,6 +76,12 @@ def test_successful_purchase_with_shipping_address assert_equal 'Approved', response.message end + def test_successful_purchase_with_billing_email + response = @gateway.purchase(@amount, @credit_card, @options.merge(email: 'test@example.com')) + assert_success response + assert_equal 'Approved', response.message + end + def test_successful_purchase_with_card_present_code response = @gateway.purchase(@amount, @credit_card, @options.merge(card_present_code: 'Present')) assert_success response @@ -116,6 +148,18 @@ def test_successful_purchase_with_merchant_descriptor assert_equal 'Approved', response.message end + def test_successful_purchase_with_google_pay + response = @gateway.purchase(@amount, @google_pay_network_token, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_apple_pay + response = @gateway.purchase(@amount, @apple_pay_network_token, @options) + assert_success response + assert_equal 'Approved', response.message + end + def test_successful_authorize_and_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth diff --git a/test/remote/gateways/remote_eway_rapid_test.rb b/test/remote/gateways/remote_eway_rapid_test.rb index 2778f1d5c4e..3113dec205d 100644 --- a/test/remote/gateways/remote_eway_rapid_test.rb +++ b/test/remote/gateways/remote_eway_rapid_test.rb @@ -112,7 +112,9 @@ def test_successful_purchase_with_shipping_address end def test_fully_loaded_purchase - response = @gateway.purchase(@amount, @credit_card, + response = @gateway.purchase( + @amount, + @credit_card, redirect_url: 'http://awesomesauce.com', ip: '0.0.0.0', application_id: 'Woohoo', @@ -148,7 +150,8 @@ def test_fully_loaded_purchase country: 'US', phone: '1115555555', fax: '1115556666' - }) + } + ) assert_success response end diff --git a/test/remote/gateways/remote_eway_test.rb b/test/remote/gateways/remote_eway_test.rb index 64f536d176f..4c306709656 100644 --- a/test/remote/gateways/remote_eway_test.rb +++ b/test/remote/gateways/remote_eway_test.rb @@ -4,9 +4,7 @@ class EwayTest < Test::Unit::TestCase def setup @gateway = EwayGateway.new(fixtures(:eway)) @credit_card_success = credit_card('4444333322221111') - @credit_card_fail = credit_card('1234567812345678', - month: Time.now.month, - year: Time.now.year - 1) + @credit_card_fail = credit_card('1234567812345678', month: Time.now.month, year: Time.now.year - 1) @params = { order_id: '1230123', diff --git a/test/remote/gateways/remote_firstdata_e4_test.rb b/test/remote/gateways/remote_firstdata_e4_test.rb index 9666a8637d7..e8a84ac81da 100755 --- a/test/remote/gateways/remote_firstdata_e4_test.rb +++ b/test/remote/gateways/remote_firstdata_e4_test.rb @@ -26,9 +26,11 @@ def test_successful_purchase end def test_successful_purchase_with_network_tokenization - @credit_card = network_tokenization_credit_card('4242424242424242', + @credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', - verification_value: nil) + verification_value: nil + ) assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'Transaction Normal - Approved', response.message diff --git a/test/remote/gateways/remote_firstdata_e4_v27_test.rb b/test/remote/gateways/remote_firstdata_e4_v27_test.rb index 8b41dc9013a..ef3f1ddb6ea 100644 --- a/test/remote/gateways/remote_firstdata_e4_v27_test.rb +++ b/test/remote/gateways/remote_firstdata_e4_v27_test.rb @@ -27,9 +27,11 @@ def test_successful_purchase end def test_successful_purchase_with_network_tokenization - @credit_card = network_tokenization_credit_card('4242424242424242', + @credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', - verification_value: nil) + verification_value: nil + ) assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'Transaction Normal - Approved', response.message diff --git a/test/remote/gateways/remote_global_collect_test.rb b/test/remote/gateways/remote_global_collect_test.rb index 7347fe2fb24..cd8efed3c02 100644 --- a/test/remote/gateways/remote_global_collect_test.rb +++ b/test/remote/gateways/remote_global_collect_test.rb @@ -6,12 +6,43 @@ def setup @gateway_preprod = GlobalCollectGateway.new(fixtures(:global_collect_preprod)) @gateway_preprod.options[:url_override] = 'preproduction' + @gateway_direct = GlobalCollectGateway.new(fixtures(:global_collect_direct)) + @gateway_direct.options[:url_override] = 'ogone_direct' + @amount = 100 @credit_card = credit_card('4567350000427977') + @credit_card_challenge_3ds2 = credit_card('4874970686672022') @naranja_card = credit_card('5895620033330020', brand: 'naranja') @cabal_card = credit_card('6271701225979642', brand: 'cabal') @declined_card = credit_card('5424180279791732') @preprod_card = credit_card('4111111111111111') + @apple_pay = network_tokenization_credit_card( + '4567350000427977', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: Time.new.year + 2, + first_name: 'John', + last_name: 'Smith', + eci: '05', + source: :apple_pay + ) + + @google_pay = network_tokenization_credit_card( + '4567350000427977', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: Time.new.year + 2, + source: :google_pay, + transaction_id: '123456789', + eci: '05' + ) + + @google_pay_pan_only = credit_card( + '4567350000427977', + month: '01', + year: Time.new.year + 2 + ) + @accepted_amount = 4005 @rejected_amount = 2997 @options = { @@ -42,6 +73,14 @@ def test_successful_purchase assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status'] end + def test_successful_purchase_ogone_direct + options = @preprod_options.merge(requires_approval: false, currency: 'EUR') + response = @gateway_direct.purchase(@accepted_amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'PENDING_CAPTURE', response.params['payment']['status'] + end + def test_successful_purchase_with_naranja options = @preprod_options.merge(requires_approval: false, currency: 'ARS') response = @gateway_preprod.purchase(1000, @naranja_card, options) @@ -58,6 +97,39 @@ def test_successful_purchase_with_cabal assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status'] end + def test_successful_purchase_with_apple_pay + options = @preprod_options.merge(requires_approval: false, currency: 'USD') + response = @gateway_preprod.purchase(4500, @apple_pay, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status'] + end + + def test_successful_purchase_with_google_pay + options = @preprod_options.merge(requires_approval: false) + response = @gateway_preprod.purchase(4500, @google_pay, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status'] + end + + def test_successful_purchase_with_google_pay_pan_only + options = @preprod_options.merge(requires_approval: false, customer: 'GP1234ID', google_pay_pan_only: true) + response = @gateway_preprod.purchase(4500, @google_pay_pan_only, options) + + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status'] + end + + def test_unsuccessful_purchase_with_google_pay_pan_only + options = @preprod_options.merge(requires_approval: false, google_pay_pan_only: true, customer: '') + response = @gateway_preprod.purchase(4500, @google_pay_pan_only, options) + + assert_failure response + assert_equal 'order.customer.merchantCustomerId is missing for UCOF', response.message + end + def test_successful_purchase_with_fraud_fields options = @options.merge( fraud_fields: { @@ -91,8 +163,8 @@ def test_successful_purchase_with_more_options end def test_successful_purchase_with_installments - options = @options.merge(number_of_installments: 2) - response = @gateway.purchase(@amount, @credit_card, options) + options = @preprod_options.merge(number_of_installments: 2, currency: 'EUR') + response = @gateway_direct.purchase(@amount, @credit_card, options) assert_success response assert_equal 'Succeeded', response.message end @@ -105,14 +177,19 @@ def test_successful_purchase_with_requires_approval_true response = @gateway.purchase(@amount, @credit_card, options) assert_success response assert_equal 'Succeeded', response.message - assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status'] end # When requires_approval is false, `purchase` will only make an `auth` call # to request capture (and no subsequent `capture` call). - def test_successful_purchase_with_requires_approval_false + def test_successful_purchase_with_requires_approval_false_ogone_direct options = @options.merge(requires_approval: false) + response = @gateway_direct.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_requires_approval_false + options = @options.merge(requires_approval: false) response = @gateway.purchase(@amount, @credit_card, options) assert_success response assert_equal 'Succeeded', response.message @@ -139,6 +216,56 @@ def test_successful_authorize_via_normalized_3ds2_fields assert_equal 'Succeeded', response.message end + def test_successful_authorize_via_3ds2_fields_direct_api + options = @options.merge( + currency: 'EUR', + phone: '5555555555', + three_d_secure: { + version: '2.1.0', + eci: '05', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', + acs_transaction_id: '13c701a3-5a88-4c45-89e9-ef65e50a8bf9', + cavv_algorithm: 1, + authentication_response_status: 'Y', + challenge_indicator: 'no-challenge-requested', + flow: 'frictionless' + } + ) + + response = @gateway_direct.authorize(@amount, @credit_card, options) + assert_success response + assert_match 'PENDING_CAPTURE', response.params['payment']['status'] + assert_match 'jJ81HADVRtXfCBATEp01CJUAAAA=', response.params['payment']['paymentOutput']['cardPaymentMethodSpecificOutput']['threeDSecureResults']['cavv'] + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_via_3ds2_fields_direct_api_challenge + options = @options.merge( + currency: 'EUR', + phone: '5555555555', + is_recurring: true, + skip_authentication: false, + three_d_secure: { + version: '2.1.0', + eci: '05', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', + acs_transaction_id: '13c701a3-5a88-4c45-89e9-ef65e50a8bf9', + cavv_algorithm: 1, + authentication_response_status: 'Y', + challenge_indicator: 'challenge-required' + } + ) + + response = @gateway_direct.purchase(@amount, @credit_card_challenge_3ds2, options) + assert_success response + assert_match 'CAPTURE_REQUESTED', response.params['status'] + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_airline_data options = @options.merge( airline_data: { @@ -151,6 +278,7 @@ def test_successful_purchase_with_airline_data is_third_party: 'true', issue_date: 'tday', merchant_customer_id: 'MIDs', + agent_numeric_code: '12345', passengers: [ { first_name: 'Randi', surname: 'Smith', @@ -267,13 +395,29 @@ def test_successful_purchase_with_blank_name assert_equal 'Succeeded', response.message end + def test_unsuccessful_purchase_with_blank_name_ogone_direct + credit_card = credit_card('4567350000427977', { first_name: nil, last_name: nil }) + + response = @gateway_direct.purchase(@amount, credit_card, @options) + assert_failure response + assert_equal 'PARAMETER_NOT_FOUND_IN_REQUEST', response.message + end + def test_successful_purchase_with_pre_authorization_flag response = @gateway.purchase(@accepted_amount, @credit_card, @options.merge(pre_authorization: true)) assert_success response assert_equal 'Succeeded', response.message end - def test_successful_purchase_with_truncated_address + def test_successful_purchase_with_payment_product_id + options = @preprod_options.merge(requires_approval: false, currency: 'ARS') + response = @gateway_preprod.purchase(1000, @cabal_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_equal 135, response.params['payment']['paymentOutput']['cardPaymentMethodSpecificOutput']['paymentProductId'] + end + + def test_successful_purchase_with_truncated_split_address response = @gateway.purchase(@amount, @credit_card, @long_address) assert_success response assert_equal 'Succeeded', response.message @@ -285,6 +429,12 @@ def test_failed_purchase assert_equal 'Not authorised', response.message end + def test_failed_purchase_ogone_direct + response = @gateway_direct.purchase(@rejected_amount, @declined_card, @options) + assert_failure response + assert_equal 'cardPaymentMethodSpecificInput.card.cardNumber does not match with cardPaymentMethodSpecificInput.paymentProductId.', response.message + end + def test_successful_authorize_and_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -306,13 +456,18 @@ def test_failed_authorize assert_equal 'Not authorised', response.message end + def test_failed_authorize_ogone_direct + response = @gateway_direct.authorize(@amount, @declined_card, @options) + assert_failure response + assert_equal 'cardPaymentMethodSpecificInput.card.cardNumber does not match with cardPaymentMethodSpecificInput.paymentProductId.', response.message + end + def test_partial_capture auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth assert capture = @gateway.capture(@amount - 1, auth.authorization) assert_success capture - assert_equal 99, capture.params['payment']['paymentOutput']['amountOfMoney']['amount'] end def test_failed_capture @@ -354,6 +509,15 @@ def test_successful_void assert_equal 'Succeeded', void.message end + def test_successful_void_ogone_direct + auth = @gateway_direct.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway_direct.void(auth.authorization) + assert_success void + assert_equal 'Succeeded', void.message + end + def test_failed_void response = @gateway.void('123') assert_failure response @@ -372,18 +536,40 @@ def test_failed_repeat_void assert_failure repeat_void end + def test_successful_inquire + response = @gateway.purchase(@accepted_amount, @credit_card, @options) + assert_success response + + response = @gateway.inquire(response.params['payment']['id']) + assert_success response + assert_equal 'Succeeded', response.message + assert response.authorization + end + def test_successful_verify response = @gateway.verify(@credit_card, @options) assert_success response assert_equal 'Succeeded', response.message end + def test_successful_verify_ogone_direct + response = @gateway_direct.verify(@credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response assert_equal 'Not authorised', response.message end + def test_failed_verify_ogone_direct + response = @gateway_direct.verify(@declined_card, @options) + assert_failure response + assert_equal false, response.params['paymentResult']['payment']['statusOutput']['isAuthorized'] + end + def test_invalid_login gateway = GlobalCollectGateway.new(merchant_id: '', api_key_id: '', secret_api_key: '') response = gateway.purchase(@amount, @credit_card, @options) @@ -401,6 +587,26 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:secret_api_key], transcript) end + def test_scrub_google_payment + options = @preprod_options.merge(requires_approval: false) + transcript = capture_transcript(@gateway) do + @gateway_preprod.purchase(@amount, @google_pay, options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@google_pay.payment_cryptogram, transcript) + assert_scrubbed(@google_pay.number, transcript) + end + + def test_scrub_apple_payment + options = @preprod_options.merge(requires_approval: false) + transcript = capture_transcript(@gateway) do + @gateway_preprod.purchase(@amount, @apple_pay, options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@apple_pay.payment_cryptogram, transcript) + assert_scrubbed(@apple_pay.number, transcript) + end + def test_successful_preprod_auth_and_capture options = @preprod_options.merge(requires_approval: true) auth = @gateway_preprod.authorize(@accepted_amount, @preprod_card, options) diff --git a/test/remote/gateways/remote_hps_test.rb b/test/remote/gateways/remote_hps_test.rb index 4b8f7229bfc..9f7a0e08c24 100644 --- a/test/remote/gateways/remote_hps_test.rb +++ b/test/remote/gateways/remote_hps_test.rb @@ -359,11 +359,13 @@ def test_transcript_scrubbing end def test_transcript_scrubbing_with_cryptogram - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :apple_pay) + source: :apple_pay + ) transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, credit_card, @options) end @@ -384,126 +386,150 @@ def test_account_number_scrubbing end def test_successful_purchase_with_apple_pay_raw_cryptogram_with_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message end def test_successful_purchase_with_apple_pay_raw_cryptogram_without_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message end def test_successful_auth_with_apple_pay_raw_cryptogram_with_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message end def test_successful_auth_with_apple_pay_raw_cryptogram_without_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message end def test_successful_purchase_with_android_pay_raw_cryptogram_with_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :android_pay) + source: :android_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message end def test_successful_purchase_with_android_pay_raw_cryptogram_without_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :android_pay) + source: :android_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message end def test_successful_auth_with_android_pay_raw_cryptogram_with_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :android_pay) + source: :android_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message end def test_successful_auth_with_android_pay_raw_cryptogram_without_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :android_pay) + source: :android_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message end def test_successful_purchase_with_google_pay_raw_cryptogram_with_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :google_pay) + source: :google_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message end def test_successful_purchase_with_google_pay_raw_cryptogram_without_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :google_pay) + source: :google_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message end def test_successful_auth_with_google_pay_raw_cryptogram_with_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :google_pay) + source: :google_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message end def test_successful_auth_with_google_pay_raw_cryptogram_without_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :google_pay) + source: :google_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message diff --git a/test/remote/gateways/remote_ipg_test.rb b/test/remote/gateways/remote_ipg_test.rb index d92ef2a5ace..9a81291b0c5 100644 --- a/test/remote/gateways/remote_ipg_test.rb +++ b/test/remote/gateways/remote_ipg_test.rb @@ -5,9 +5,9 @@ def setup @gateway = IpgGateway.new(fixtures(:ipg)) @amount = 100 - @credit_card = credit_card('5165850000000008', brand: 'mastercard', verification_value: '530', month: '12', year: '2022') + @credit_card = credit_card('5165850000000008', brand: 'mastercard', verification_value: '987', month: '12', year: '2029') @declined_card = credit_card('4000300011112220', brand: 'mastercard', verification_value: '652', month: '12', year: '2022') - @visa_card = credit_card('4704550000000005', brand: 'visa', verification_value: '123', month: '12', year: '2022') + @visa_card = credit_card('4704550000000005', brand: 'visa', verification_value: '123', month: '12', year: '2029') @options = { currency: 'ARS' } @@ -96,10 +96,17 @@ def test_successful_purchase_with_3ds2_options def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'DECLINED', response.message + assert_match 'DECLINED', response.message assert_equal 'SGS-050005', response.error_code end + def test_failed_purchase_with_passed_in_store_id + # passing in a bad store id results in a 401 unauthorized error + assert_raises(ActiveMerchant::ResponseError) do + @gateway.purchase(@amount, @declined_card, @options.merge({ store_id: '1234' })) + end + end + def test_successful_authorize_and_capture order_id = generate_unique_id response = @gateway.authorize(@amount, @credit_card, @options.merge!({ order_id: order_id })) @@ -114,14 +121,14 @@ def test_successful_authorize_and_capture def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_equal 'DECLINED', response.message + assert_equal 'DECLINED, Do not honour', response.message assert_equal 'SGS-050005', response.error_code end def test_failed_capture response = @gateway.capture(@amount, '', @options) assert_failure response - assert_equal 'FAILED', response.message + assert_match 'FAILED', response.message assert_equal 'SGS-005001', response.error_code end @@ -152,7 +159,7 @@ def test_successful_refund def test_failed_refund response = @gateway.refund(@amount, '', @options) assert_failure response - assert_equal 'FAILED', response.message + assert_match 'FAILED', response.message assert_equal 'SGS-005001', response.error_code end @@ -165,7 +172,7 @@ def test_successful_verify def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response - assert_equal 'DECLINED', response.message + assert_match 'DECLINED', response.message assert_equal 'SGS-050005', response.error_code end diff --git a/test/remote/gateways/remote_iveri_test.rb b/test/remote/gateways/remote_iveri_test.rb index 2b6ed046705..0ced8b40be3 100644 --- a/test/remote/gateways/remote_iveri_test.rb +++ b/test/remote/gateways/remote_iveri_test.rb @@ -24,6 +24,16 @@ def test_successful_purchase assert_equal '100', response.params['amount'] end + def test_successful_purchase_with_iveri_url + credentials = fixtures(:iveri_url).merge(url_override: 'iveri') + @gateway = IveriGateway.new(credentials) + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Succeeded', response.message + assert_equal '100', response.params['amount'] + end + def test_successful_purchase_with_more_options options = { ip: '127.0.0.1', @@ -164,4 +174,36 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:cert_id], transcript) end + + def test_successful_authorize_with_3ds_v1_options + @options[:three_d_secure] = { + version: '1.0', + cavv: 'FHhirTpN0Aefs4rIzTlheBByD77J', + eci: '02', + xid: SecureRandom.alphanumeric(28), + enrolled: 'true', + authentication_response_status: 'Y' + } + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_authorize_with_3ds_v2_options + @options[:three_d_secure] = { + version: '2.1.0', + cavv: 'FHhirTpN0Aefs4rIzTlheBByD77J', + eci: '02', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'Y', + authentication_response_status: 'Y' + } + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Succeeded', response.message + assert_equal '100', response.params['amount'] + end end diff --git a/test/remote/gateways/remote_kushki_test.rb b/test/remote/gateways/remote_kushki_test.rb index 47e07a78bcd..14ab10b18c9 100644 --- a/test/remote/gateways/remote_kushki_test.rb +++ b/test/remote/gateways/remote_kushki_test.rb @@ -15,6 +15,13 @@ def test_successful_purchase assert_match %r(^\d+$), response.authorization end + def test_successful_purchase_brazil + response = @gateway.purchase(@amount, @credit_card, { currency: 'BRL' }) + assert_success response + assert_equal 'Succeeded', response.message + assert_match %r(^\d+$), response.authorization + end + def test_successful_purchase_with_options options = { currency: 'USD', @@ -36,7 +43,11 @@ def test_successful_purchase_with_options metadata: { productos: 'bananas', nombre_apellido: 'Kirk' - } + }, + months: 2, + deferred_grace_months: '05', + deferred_credit_type: '01', + deferred_months: 3 } amount = 100 * ( @@ -52,6 +63,74 @@ def test_successful_purchase_with_options assert_match %r(^\d+$), response.authorization end + def test_successful_purchase_with_extra_taxes_cop + options = { + currency: 'COP', + amount: { + subtotal_iva_0: '4.95', + subtotal_iva: '10', + iva: '1.54', + ice: '3.50', + extra_taxes: { + propina: 0.1, + tasa_aeroportuaria: 0.2, + agencia_de_viaje: 0.3, + iac: 0.4 + } + } + } + + amount = 100 * ( + options[:amount][:subtotal_iva_0].to_f + + options[:amount][:subtotal_iva].to_f + + options[:amount][:iva].to_f + + options[:amount][:ice].to_f + + options[:amount][:extra_taxes][:propina].to_f + + options[:amount][:extra_taxes][:tasa_aeroportuaria].to_f + + options[:amount][:extra_taxes][:agencia_de_viaje].to_f + + options[:amount][:extra_taxes][:iac].to_f + ) + + response = @gateway.purchase(amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_match %r(^\d+$), response.authorization + end + + def test_successful_purchase_with_extra_taxes_usd + options = { + currency: 'USD', + amount: { + subtotal_iva_0: '4.95', + subtotal_iva: '10', + iva: '1.54', + ice: '3.50', + extra_taxes: { + propina: 0.1, + tasa_aeroportuaria: 0.2, + agencia_de_viaje: 0.3, + iac: 0.4 + } + } + } + + amount = 100 * ( + options[:amount][:subtotal_iva_0].to_f + + options[:amount][:subtotal_iva].to_f + + options[:amount][:iva].to_f + + options[:amount][:ice].to_f + + options[:amount][:extra_taxes][:propina].to_f + + options[:amount][:extra_taxes][:tasa_aeroportuaria].to_f + + options[:amount][:extra_taxes][:agencia_de_viaje].to_f + + options[:amount][:extra_taxes][:iac].to_f + ) + + response = @gateway.purchase(amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_match %r(^\d+$), response.authorization + end + def test_failed_purchase options = { amount: { @@ -65,13 +144,19 @@ def test_failed_purchase end def test_successful_authorize - # Kushki only allows preauthorization for PEN, CLP, and UF. response = @gateway.authorize(@amount, @credit_card, { currency: 'PEN' }) assert_success response assert_equal 'Succeeded', response.message assert_match %r(^\d+$), response.authorization end + def test_successful_authorize_brazil + response = @gateway.authorize(@amount, @credit_card, { currency: 'BRL' }) + assert_success response + assert_equal 'Succeeded', response.message + assert_match %r(^\d+$), response.authorization + end + def test_approval_code_comes_back_when_passing_full_response options = { full_response: true @@ -94,6 +179,101 @@ def test_failed_authorize assert_equal 'Monto de la transacción es diferente al monto de la venta inicial', response.message end + def test_successful_3ds2_authorize_with_visa_card + options = { + currency: 'PEN', + three_d_secure: { + version: '2.2.0', + cavv: 'AAABBoVBaZKAR3BkdkFpELpWIiE=', + xid: 'NEpab1F1MEdtaWJ2bEY3ckYxQzE=', + eci: '07' + } + } + response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_match %r(^\d+$), response.authorization + end + + def test_successful_3ds2_authorize_with_visa_card_with_optional_xid + options = { + currency: 'PEN', + three_d_secure: { + version: '2.2.0', + cavv: 'AAABBoVBaZKAR3BkdkFpELpWIiE=', + eci: '07' + } + } + response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + assert_match %r(^\d+$), response.authorization + end + + def test_successful_3ds2_authorize_with_master_card + options = { + currency: 'PEN', + three_d_secure: { + version: '2.2.0', + cavv: 'AAABBoVBaZKAR3BkdkFpELpWIiE=', + eci: '00', + ds_transaction_id: 'b23e0264-1209-41L6-Jca4-b82143c1a782' + } + } + + credit_card = credit_card('5223450000000007', brand: 'master', verification_value: '777') + response = @gateway.authorize(@amount, credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_3ds2_purchase + options = { + three_d_secure: { + version: '2.2.0', + cavv: 'AAABBoVBaZKAR3BkdkFpELpWIiE=', + xid: 'NEpab1F1MEdtaWJ2bEY3ckYxQzE=', + eci: '07' + } + } + + response = @gateway.purchase(@amount, @credit_card, options) + + assert_success response + assert_equal 'Succeeded', response.message + assert_match %r(^\d+$), response.authorization + end + + def test_failed_3ds2_authorize + options = { + currency: 'PEN', + three_d_secure: { + version: '2.2.0', + authentication_response_status: 'Y', + cavv: 'AAABBoVBaZKAR3BkdkFpELpWIiE=', + xid: 'NEpab1F1MEdtaWJ2bEY3ckYxQzE=' + } + } + response = @gateway.authorize(@amount, @credit_card, options) + assert_failure response + assert_equal 'K001', response.responses.last.error_code + end + + def test_failed_3ds2_authorize_with_different_card + options = { + currency: 'PEN', + three_d_secure: { + version: '2.2.0', + cavv: 'AAABBoVBaZKAR3BkdkFpELpWIiE=', + xid: 'NEpab1F1MEdtaWJ2bEY3ckYxQzE=' + } + } + credit_card = credit_card('6011111111111117', brand: 'discover', verification_value: '777') + assert_raise ArgumentError do + @gateway.authorize(@amount, credit_card, options) + end + end + def test_successful_capture auth = @gateway.authorize(@amount, @credit_card) assert_success auth diff --git a/test/remote/gateways/remote_linkpoint_test.rb b/test/remote/gateways/remote_linkpoint_test.rb index bb6db34f34f..efcffa07dec 100644 --- a/test/remote/gateways/remote_linkpoint_test.rb +++ b/test/remote/gateways/remote_linkpoint_test.rb @@ -100,12 +100,15 @@ def test_successfull_purchase_with_item_entity end def test_successful_recurring_payment - assert response = @gateway.recurring(2400, @credit_card, + assert response = @gateway.recurring( + 2400, + @credit_card, order_id: generate_unique_id, installments: 12, startdate: 'immediate', periodicity: :monthly, - billing_address: address) + billing_address: address + ) assert_success response assert_equal 'APPROVED', response.params['approved'] diff --git a/test/remote/gateways/remote_litle_test.rb b/test/remote/gateways/remote_litle_test.rb index 6f3840071ca..80241a8b361 100644 --- a/test/remote/gateways/remote_litle_test.rb +++ b/test/remote/gateways/remote_litle_test.rb @@ -92,6 +92,8 @@ def setup routing_number: '011100012', account_number: '1099999998' ) + + @declined_card = credit_card('4488282659650110', first_name: nil, last_name: 'REFUSED') end def test_successful_authorization @@ -143,14 +145,24 @@ def test_successful_authorization_with_echeck assert_equal 'Approved', response.message end - def test_avs_and_cvv_result + def test_avs_result + @credit_card1.number = '4200410886320101' + assert response = @gateway.authorize(10010, @credit_card1, @options) + + assert_equal 'Z', response.avs_result['code'] + end + + def test__cvv_result + @credit_card1.number = '4100521234567000' assert response = @gateway.authorize(10010, @credit_card1, @options) - assert_equal 'X', response.avs_result['code'] - assert_equal 'M', response.cvv_result['code'] + + assert_equal 'P', response.cvv_result['code'] end def test_unsuccessful_authorization - assert response = @gateway.authorize(60060, @credit_card2, + assert response = @gateway.authorize( + 60060, + @declined_card, { order_id: '6', billing_address: { @@ -161,7 +173,8 @@ def test_unsuccessful_authorization zip: '03038', country: 'US' } - }) + } + ) assert_failure response assert_equal 'Insufficient Funds', response.message end @@ -206,6 +219,17 @@ def test_successful_purchase_with_debt_repayment_flag assert_equal 'Approved', response.message end + def test_successful_purchase_with_fraud_filter_override_flag + assert response = @gateway.purchase(10010, @credit_card1, @options.merge(fraud_filter_override: true)) + assert_success response + assert_equal 'Approved', response.message + end + + def test_failed_purchase_when_fraud_filter_override_flag_not_sent_as_boolean + assert response = @gateway.purchase(10010, @credit_card1, @options.merge(fraud_filter_override: 'hey')) + assert_failure response + end + def test_successful_purchase_with_3ds_fields options = @options.merge({ order_source: '3dsAuthenticated', @@ -220,7 +244,7 @@ def test_successful_purchase_with_3ds_fields def test_successful_purchase_with_apple_pay assert response = @gateway.purchase(10010, @decrypted_apple_pay) assert_success response - assert_equal 'Approved', response.message + assert_equal 'Partially Approved: The authorized amount is less than the requested amount.', response.message end def test_successful_purchase_with_android_pay @@ -359,7 +383,7 @@ def test_successful_purchase_with_echeck end def test_unsuccessful_purchase - assert response = @gateway.purchase(60060, @credit_card2, { + assert response = @gateway.purchase(60060, @declined_card, { order_id: '6', billing_address: { name: 'Joe Green', @@ -387,6 +411,8 @@ def test_authorize_capture_refund_void assert_success refund assert_equal 'Approved', refund.message + sleep 40.seconds + assert void = @gateway.void(refund.authorization) assert_success void assert_equal 'Approved', void.message @@ -411,7 +437,7 @@ def test_authorize_and_capture_with_stored_credential_recurring ) assert auth = @gateway.authorize(4999, credit_card, initial_options) assert_success auth - assert_equal 'Approved', auth.message + assert_equal 'Transaction Received: This is sent to acknowledge that the submitted transaction has been received.', auth.message assert network_transaction_id = auth.params['networkTransactionId'] assert capture = @gateway.capture(4999, auth.authorization) @@ -429,7 +455,7 @@ def test_authorize_and_capture_with_stored_credential_recurring ) assert auth = @gateway.authorize(4999, credit_card, used_options) assert_success auth - assert_equal 'Approved', auth.message + assert_equal 'Transaction Received: This is sent to acknowledge that the submitted transaction has been received.', auth.message assert capture = @gateway.capture(4999, auth.authorization) assert_success capture @@ -631,9 +657,9 @@ def test_void_authorization end def test_unsuccessful_void - assert void = @gateway.void('123456789012345360;authorization;100') + assert void = @gateway.void('1234567890r2345360;authorization;100') assert_failure void - assert_equal 'No transaction found with specified Transaction Id', void.message + assert_match(/^Error validating xml data against the schema/, void.message) end def test_successful_credit @@ -699,15 +725,15 @@ def test_nil_amount_capture end def test_capture_unsuccessful - assert capture_response = @gateway.capture(10010, '123456789012345360') + assert capture_response = @gateway.capture(10010, '123456789w123') assert_failure capture_response - assert_equal 'No transaction found with specified Transaction Id', capture_response.message + assert_match(/^Error validating xml data against the schema/, capture_response.message) end def test_refund_unsuccessful - assert credit_response = @gateway.refund(10010, '123456789012345360') + assert credit_response = @gateway.refund(10010, '123456789w123') assert_failure credit_response - assert_equal 'No transaction found with specified Transaction Id', credit_response.message + assert_match(/^Error validating xml data against the schema/, credit_response.message) end def test_void_unsuccessful @@ -722,10 +748,8 @@ def test_store_successful assert_success store_response assert_equal 'Account number was successfully registered', store_response.message - assert_equal '445711', store_response.params['bin'] - assert_equal 'VI', store_response.params['type'] assert_equal '801', store_response.params['response'] - assert_equal '1111222233330123', store_response.params['litleToken'] + assert_equal '1111222233334444', store_response.params['litleToken'] end def test_store_with_paypage_registration_id_successful @@ -739,11 +763,11 @@ def test_store_with_paypage_registration_id_successful end def test_store_unsuccessful - credit_card = CreditCard.new(@credit_card_hash.merge(number: '4457119999999999')) + credit_card = CreditCard.new(@credit_card_hash.merge(number: '4100282090123000')) assert store_response = @gateway.store(credit_card, order_id: '51') assert_failure store_response - assert_equal 'Credit card number was invalid', store_response.message + assert_equal 'Credit card Number was invalid', store_response.message assert_equal '820', store_response.params['response'] end @@ -757,7 +781,7 @@ def test_store_and_purchase_with_token_successful assert response = @gateway.purchase(10010, token) assert_success response - assert_equal 'Approved', response.message + assert_equal 'Partially Approved: The authorized amount is less than the requested amount.', response.message end def test_purchase_with_token_and_date_successful @@ -769,7 +793,7 @@ def test_purchase_with_token_and_date_successful assert response = @gateway.purchase(10010, token, { basis_expiration_month: '01', basis_expiration_year: '2024' }) assert_success response - assert_equal 'Approved', response.message + assert_equal 'Partially Approved: The authorized amount is less than the requested amount.', response.message end def test_echeck_store_and_purchase @@ -782,7 +806,7 @@ def test_echeck_store_and_purchase assert response = @gateway.purchase(10010, token) assert_success response - assert_equal 'Approved', response.message + assert_equal 'Partially Approved: The authorized amount is less than the requested amount.', response.message end def test_successful_verify diff --git a/test/remote/gateways/remote_mercado_pago_test.rb b/test/remote/gateways/remote_mercado_pago_test.rb index e4e6426ffb4..9aab14911f3 100644 --- a/test/remote/gateways/remote_mercado_pago_test.rb +++ b/test/remote/gateways/remote_mercado_pago_test.rb @@ -10,26 +10,31 @@ def setup @amount = 500 @credit_card = credit_card('5031433215406351') @colombian_card = credit_card('4013540682746260') - @elo_credit_card = credit_card('5067268650517446', + @elo_credit_card = credit_card( + '5067268650517446', month: 10, year: exp_year, first_name: 'John', last_name: 'Smith', - verification_value: '737') - @cabal_credit_card = credit_card('6035227716427021', + verification_value: '737' + ) + @cabal_credit_card = credit_card( + '6035227716427021', month: 10, year: exp_year, first_name: 'John', last_name: 'Smith', - verification_value: '737') - @naranja_credit_card = credit_card('5895627823453005', + verification_value: '737' + ) + @naranja_credit_card = credit_card( + '5895627823453005', month: 10, year: exp_year, first_name: 'John', last_name: 'Smith', - verification_value: '123') - @declined_card = credit_card('5031433215406351', - first_name: 'OTHE') + verification_value: '123' + ) + @declined_card = credit_card('5031433215406351', first_name: 'OTHE') @options = { billing_address: address, shipping_address: address, @@ -320,6 +325,26 @@ def test_failed_verify assert_match %r{cc_rejected_other_reason}, response.message end + def test_successful_inquire_with_id + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert_equal 'pending_capture', auth.message + + assert inquire = @gateway.inquire(auth.authorization) + assert_success inquire + assert_equal auth.message, inquire.message + end + + def test_successful_inquire_with_external_reference + auth = @gateway.authorize(@amount, @credit_card, @options.merge(order_id: 'abcd1234')) + assert_success auth + assert auth.params['external_reference'] = 'abcd1234' + + assert inquire = @gateway.inquire(nil, { external_reference: 'abcd1234' }) + assert_success inquire + assert_equal auth.authorization, inquire.authorization + end + def test_invalid_login gateway = MercadoPagoGateway.new(access_token: '') diff --git a/test/remote/gateways/remote_merchant_e_solutions_test.rb b/test/remote/gateways/remote_merchant_e_solutions_test.rb index 969d4cd14dd..c514aa6e3a2 100644 --- a/test/remote/gateways/remote_merchant_e_solutions_test.rb +++ b/test/remote/gateways/remote_merchant_e_solutions_test.rb @@ -20,9 +20,20 @@ def setup state: 'MT', country: 'US', zip: '55555', - phone: '555-555-5555' + phone: '555-555-5555', + recurring_pmt_num: 11, + recurring_pmt_count: 10 } } + @stored_credential_options = { + moto_ecommerce_ind: '7', + client_reference_number: '345892', + recurring_pmt_num: 11, + recurring_pmt_count: 10, + card_on_file: 'Y', + cit_mit_indicator: 'C101', + account_data_source: 'Y' + } end # MES has a race condition with immediately trying to operate on an @@ -37,6 +48,12 @@ def test_successful_purchase assert_equal 'This transaction has been approved', response.message end + def test_successful_purchase_with_moto_ecommerce_ind + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@stored_credential_options)) + assert_success response + assert_equal 'This transaction has been approved', response.message + end + def test_unsuccessful_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -71,7 +88,8 @@ def test_failed_capture def test_store_purchase_unstore assert store = @gateway.store(@credit_card) assert_success store - assert_equal 'This transaction has been approved', store.message + assert_equal 'Card Ok', store.message + assert_equal store.authorization, store.params['card_id'] assert purchase = @gateway.purchase(@amount, store.authorization, @options) assert_success purchase assert_equal 'This transaction has been approved', purchase.message @@ -183,13 +201,6 @@ def test_invalid_login assert_failure response end - def test_connection_failure_404_notfound_with_purchase - @gateway.test_url = 'https://cert.merchante-solutions.com/mes-api/tridentApiasdasd' - assert response = @gateway.purchase(@amount, @credit_card, @options) - assert_failure response - assert_equal 'Failed with 404 Not Found', response.message - end - def test_successful_purchase_with_3dsecure_params options = @options.merge( { xid: 'ERERERERERERERERERERERERERE=', @@ -200,6 +211,12 @@ def test_successful_purchase_with_3dsecure_params assert_equal 'This transaction has been approved', response.message end + def test_successful_verify + assert response = @gateway.verify(@credit_card, @options.merge({ verify_amount: 0 })) + assert_success response + assert_equal 'Card Ok', response.message + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card, @options) diff --git a/test/remote/gateways/remote_merchant_ware_version_four_test.rb b/test/remote/gateways/remote_merchant_ware_version_four_test.rb index 2d18cbeb868..b553611b7e3 100644 --- a/test/remote/gateways/remote_merchant_ware_version_four_test.rb +++ b/test/remote/gateways/remote_merchant_ware_version_four_test.rb @@ -69,9 +69,11 @@ def test_purchase_and_reference_purchase assert_success purchase assert purchase.authorization - assert reference_purchase = @gateway.purchase(@amount, + assert reference_purchase = @gateway.purchase( + @amount, purchase.authorization, - @reference_purchase_options) + @reference_purchase_options + ) assert_success reference_purchase assert_not_nil reference_purchase.authorization end diff --git a/test/remote/gateways/remote_monei_test.rb b/test/remote/gateways/remote_monei_test.rb index 5b6eece1c68..9d49ade34f9 100755 --- a/test/remote/gateways/remote_monei_test.rb +++ b/test/remote/gateways/remote_monei_test.rb @@ -69,7 +69,8 @@ def test_successful_purchase_with_3ds xid: 'CAACCVVUlwCXUyhQNlSXAAAAAAA=' }, ip: '77.110.174.153', - user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36' + user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36', + lang: 'en' }) response = @gateway.purchase(@amount, @credit_card, options) @@ -86,7 +87,8 @@ def test_successful_purchase_with_3ds_v2 ds_transaction_id: '7eac9571-3533-4c38-addd-00cf34af6a52' }, ip: '77.110.174.153', - user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36' + user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36', + lang: 'en' }) response = @gateway.purchase(@amount, @three_ds_2_enrolled_card, options) @@ -110,7 +112,8 @@ def test_failed_purchase_with_3ds xid: 'CAACCVVUlwCXUyhQNlSXAAAAAAA=' }, ip: '77.110.174.153', - user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36' + user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36', + lang: 'en' }) response = @gateway.purchase(@amount, @three_ds_declined_card, options) assert_failure response @@ -136,7 +139,8 @@ def test_successful_store_with_3ds_v2 ds_transaction_id: '7eac9571-3533-4c38-addd-00cf34af6a52' }, ip: '77.110.174.153', - user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36' + user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36', + lang: 'en' }) response = @gateway.store(@three_ds_2_enrolled_card, options) diff --git a/test/remote/gateways/remote_moneris_test.rb b/test/remote/gateways/remote_moneris_test.rb index 5f37b676c25..f9ed446aef4 100644 --- a/test/remote/gateways/remote_moneris_test.rb +++ b/test/remote/gateways/remote_moneris_test.rb @@ -15,6 +15,15 @@ def setup @no_liability_shift_eci = 7 @credit_card = credit_card('4242424242424242', verification_value: '012') + @network_tokenization_credit_card = network_tokenization_credit_card( + '4242424242424242', + payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + verification_value: nil + ) + @apple_pay_credit_card = @network_tokenization_credit_card + @apple_pay_credit_card.source = :apple_pay + @google_pay_credit_card = @network_tokenization_credit_card + @google_pay_credit_card.source = :google_pay @visa_credit_card_3ds = credit_card('4606633870436092', verification_value: '012') @options = { order_id: generate_unique_id, @@ -32,7 +41,9 @@ def test_successful_purchase def test_successful_cavv_purchase # See https://developer.moneris.com/livedemo/3ds2/cavv_purchase/tool/php - assert response = @gateway.purchase(@amount, @visa_credit_card_3ds, + assert response = @gateway.purchase( + @amount, + @visa_credit_card_3ds, @options.merge( three_d_secure: { version: '2', @@ -41,7 +52,8 @@ def test_successful_cavv_purchase three_ds_server_trans_id: 'd0f461f8-960f-40c9-a323-4e43a4e16aaa', ds_transaction_id: '12345' } - )) + ) + ) assert_success response assert_equal 'Approved', response.message assert_false response.authorization.blank? @@ -104,25 +116,74 @@ def test_successful_subsequent_purchase_with_credential_on_file end def test_successful_purchase_with_network_tokenization - @credit_card = network_tokenization_credit_card( - '4242424242424242', - payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', - verification_value: nil - ) - assert response = @gateway.purchase(@amount, @credit_card, @options) + assert response = @gateway.purchase(@amount, @network_tokenization_credit_card, @options) assert_success response assert_equal 'Approved', response.message assert_false response.authorization.blank? end def test_successful_purchase_with_network_tokenization_apple_pay_source - @credit_card = network_tokenization_credit_card( - '4242424242424242', - payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', - verification_value: nil, - source: :apple_pay - ) - assert response = @gateway.purchase(@amount, @credit_card, @options) + assert response = @gateway.purchase(@amount, @apple_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_network_tokenization_apple_pay_source_with_nil_order_id + @options[:order_id] = nil + assert response = @gateway.purchase(@amount, @apple_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_network_tokenization_google_pay_source + assert response = @gateway.purchase(@amount, @google_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_purchase_with_network_tokenization_google_pay_source_with_nil_order_id + @options[:order_id] = nil + assert response = @gateway.purchase(@amount, @google_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_authorize_with_network_tokenization + assert response = @gateway.authorize(@amount, @network_tokenization_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_authorize_with_network_tokenization_apple_pay_source + assert response = @gateway.authorize(@amount, @apple_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_authorize_with_network_tokenization_apple_pay_source_with_nil_order_id + @options[:order_id] = nil + assert response = @gateway.authorize(@amount, @apple_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_authorize_with_network_tokenization_google_pay_source + assert response = @gateway.authorize(@amount, @google_pay_credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + + def test_successful_authorize_with_network_tokenization_google_pay_source_with_nil_order_id + @options[:order_id] = nil + assert response = @gateway.authorize(@amount, @google_pay_credit_card, @options) assert_success response assert_equal 'Approved', response.message assert_false response.authorization.blank? @@ -172,7 +233,9 @@ def test_successful_authorization_and_void def test_successful_cavv_authorization # see https://developer.moneris.com/livedemo/3ds2/cavv_preauth/tool/php # also see https://github.com/Moneris/eCommerce-Unified-API-PHP/blob/3cd3f0bd5a92432c1b4f9727d1ca6334786d9066/Examples/CA/TestCavvPreAuth.php - response = @gateway.authorize(@amount, @visa_credit_card_3ds, + response = @gateway.authorize( + @amount, + @visa_credit_card_3ds, @options.merge( three_d_secure: { version: '2', @@ -181,7 +244,8 @@ def test_successful_cavv_authorization three_ds_server_trans_id: 'e11d4985-8d25-40ed-99d6-c3803fe5e68f', ds_transaction_id: '12345' } - )) + ) + ) assert_success response assert_equal 'Approved', response.message assert_false response.authorization.blank? @@ -190,7 +254,9 @@ def test_successful_cavv_authorization def test_successful_cavv_authorization_and_capture # see https://developer.moneris.com/livedemo/3ds2/cavv_preauth/tool/php # also see https://github.com/Moneris/eCommerce-Unified-API-PHP/blob/3cd3f0bd5a92432c1b4f9727d1ca6334786d9066/Examples/CA/TestCavvPreAuth.php - response = @gateway.authorize(@amount, @visa_credit_card_3ds, + response = @gateway.authorize( + @amount, + @visa_credit_card_3ds, @options.merge( three_d_secure: { version: '2', @@ -199,7 +265,8 @@ def test_successful_cavv_authorization_and_capture three_ds_server_trans_id: 'e11d4985-8d25-40ed-99d6-c3803fe5e68f', ds_transaction_id: '12345' } - )) + ) + ) assert_success response assert_equal 'Approved', response.message assert_false response.authorization.blank? @@ -212,7 +279,9 @@ def test_failed_cavv_authorization omit('There is no way to currently create a failed cavv authorization scenario') # see https://developer.moneris.com/livedemo/3ds2/cavv_preauth/tool/php # also see https://github.com/Moneris/eCommerce-Unified-API-PHP/blob/3cd3f0bd5a92432c1b4f9727d1ca6334786d9066/Examples/CA/TestCavvPreAuth.php - response = @gateway.authorize(@fail_amount, @visa_credit_card_3ds, + response = @gateway.authorize( + @fail_amount, + @visa_credit_card_3ds, @options.merge( three_d_secure: { version: '2', @@ -221,7 +290,8 @@ def test_failed_cavv_authorization three_ds_server_trans_id: 'e11d4985-8d25-40ed-99d6-c3803fe5e68f', ds_transaction_id: '12345' } - )) + ) + ) assert_failure response end @@ -314,8 +384,8 @@ def test_successful_store_and_purchase_with_avs assert_false response.authorization.blank? assert_equal(response.avs_result, { - 'code' => 'M', - 'message' => 'Street address and postal code match.', + 'code' => 'Y', + 'message' => 'Street address and 5-digit postal code match.', 'street_match' => 'Y', 'postal_match' => 'Y' }) diff --git a/test/remote/gateways/remote_mundipagg_test.rb b/test/remote/gateways/remote_mundipagg_test.rb index 139b5b352a2..2eacfc42fa1 100644 --- a/test/remote/gateways/remote_mundipagg_test.rb +++ b/test/remote/gateways/remote_mundipagg_test.rb @@ -5,7 +5,7 @@ def setup @gateway = MundipaggGateway.new(fixtures(:mundipagg)) @amount = 100 - @credit_card = credit_card('4000100011112224') + @credit_card = credit_card('4000000000000010') @declined_card = credit_card('4000300011112220') @sodexo_voucher = credit_card('6060704495764400', brand: 'sodexo') @@ -66,7 +66,7 @@ def test_successful_purchase_no_address @options.delete(:billing_address) response = @gateway.purchase(@amount, @credit_card, @options) assert_success response - assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + assert_equal 'Transação capturada com sucesso', response.message end def test_successful_purchase_with_more_options @@ -85,21 +85,21 @@ def test_successful_purchase_with_sodexo_voucher @options.update(holder_document: '93095135270') response = @gateway.purchase(@amount, @sodexo_voucher, @options) assert_success response - assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + assert_equal 'Transação capturada com sucesso', response.message end def test_successful_purchase_with_vr_voucher @options.update(holder_document: '93095135270') response = @gateway.purchase(@amount, @vr_voucher, @options) assert_success response - assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + assert_equal 'Transação capturada com sucesso', response.message end def test_successful_purchase_with_submerchant options = @options.update(@submerchant_options) response = @gateway.purchase(@amount, @credit_card, options) assert_success response - assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + assert_equal 'Transação capturada com sucesso', response.message end def test_failed_purchase @@ -131,7 +131,7 @@ def test_successful_authorize_with_submerchant options = @options.update(@submerchant_options) response = @gateway.authorize(@amount, @credit_card, options) assert_success response - assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + assert_equal 'Transação authorizada com sucesso', response.message end def test_failed_authorize @@ -262,7 +262,7 @@ def test_invalid_login_with_bad_api_key_overwrite def test_successful_purchase_with_api_key_overwrite response = @gateway.purchase(@amount, @credit_card, @options.merge(@authorization_secret_options)) assert_success response - assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + assert_equal 'Transação capturada com sucesso', response.message end def test_failed_store_with_top_level_errors @@ -308,13 +308,13 @@ def test_transcript_scrubbing def test_successful_purchase_with(card) response = @gateway.purchase(@amount, card, @options) assert_success response - assert_equal 'Simulator|Transação de simulação autorizada com sucesso', response.message + assert_equal 'Transação capturada com sucesso', response.message end def test_failed_purchase_with(card) response = @gateway.purchase(105200, card, @options) assert_failure response - assert_equal 'Simulator|Transação de simulada negada por falta de crédito, utilizado para realizar simulação de autorização parcial.', response.message + assert_equal 'Transação não autorizada', response.message end def test_successful_authorize_and_capture_with(card) @@ -323,13 +323,13 @@ def test_successful_authorize_and_capture_with(card) assert capture = @gateway.capture(@amount, auth.authorization) assert_success capture - assert_equal 'Simulator|Transação de simulação capturada com sucesso', capture.message + assert_equal 'Transação capturada com sucesso', capture.message end def test_failed_authorize_with(card) response = @gateway.authorize(105200, card, @options) assert_failure response - assert_equal 'Simulator|Transação de simulada negada por falta de crédito, utilizado para realizar simulação de autorização parcial.', response.message + assert_equal 'Transação não autorizada', response.message end def test_partial_capture_with(card) @@ -367,7 +367,7 @@ def test_successful_void_with(card) def test_successful_verify_with(card) response = @gateway.verify(card, @options) assert_success response - assert_match %r{Simulator|Transação de simulação autorizada com sucesso}, response.message + assert_match %r{Transação authorizada com sucesso}, response.message end def test_successful_store_and_purchase_with(card) @@ -376,6 +376,6 @@ def test_successful_store_and_purchase_with(card) assert purchase = @gateway.purchase(@amount, store.authorization, @options) assert_success purchase - assert_equal 'Simulator|Transação de simulação autorizada com sucesso', purchase.message + assert_equal 'Transação capturada com sucesso', purchase.message end end diff --git a/test/remote/gateways/remote_net_registry_test.rb b/test/remote/gateways/remote_net_registry_test.rb index 2b983e8b680..dfbb80c9ac1 100644 --- a/test/remote/gateways/remote_net_registry_test.rb +++ b/test/remote/gateways/remote_net_registry_test.rb @@ -56,9 +56,7 @@ def test_successful_authorization_and_capture assert_equal 'approved', response.params['status'] assert_match(/\A\d{6}\z/, response.authorization) - response = @gateway.capture(@amount, - response.authorization, - credit_card: @valid_creditcard) + response = @gateway.capture(@amount, response.authorization, credit_card: @valid_creditcard) assert_success response assert_equal 'approved', response.params['status'] end diff --git a/test/remote/gateways/remote_nmi_test.rb b/test/remote/gateways/remote_nmi_test.rb index ad01c314b92..7814d39dd92 100644 --- a/test/remote/gateways/remote_nmi_test.rb +++ b/test/remote/gateways/remote_nmi_test.rb @@ -10,13 +10,15 @@ def setup routing_number: '123123123', account_number: '123123123' ) - @apple_pay_card = network_tokenization_credit_card('4111111111111111', + @apple_pay_card = network_tokenization_credit_card( + '4111111111111111', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', month: '01', year: '2024', source: :apple_pay, eci: '5', - transaction_id: '123456789') + transaction_id: '123456789' + ) @options = { order_id: generate_unique_id, billing_address: address, @@ -193,6 +195,26 @@ def test_successful_purchase_with_descriptors assert response.authorization end + def test_successful_purchase_with_shipping_fields + options = @options.merge({ shipping_address: shipping_address, shipping_email: 'test@example.com' }) + + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_successful_purchase_with_surcharge + options = @options.merge({ surcharge: '1.00' }) + + assert response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert response.authorization + end + def test_failed_authorization assert response = @gateway.authorize(99, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_ogone_test.rb b/test/remote/gateways/remote_ogone_test.rb index 16c88c0e124..41ce461866d 100644 --- a/test/remote/gateways/remote_ogone_test.rb +++ b/test/remote/gateways/remote_ogone_test.rb @@ -5,11 +5,17 @@ class RemoteOgoneTest < Test::Unit::TestCase def setup @gateway = OgoneGateway.new(fixtures(:ogone)) + + # this change is according the new PSD2 guideline + # https://support.legacy.worldline-solutions.com/en/direct/faq/i-have-noticed-i-have-more-declined-transactions-status-2-than-usual-what-can-i-do + @gateway_3ds = OgoneGateway.new(fixtures(:ogone).merge(signature_encryptor: 'sha512')) @amount = 100 @credit_card = credit_card('4000100011112224') @mastercard = credit_card('5399999999999999', brand: 'mastercard') @declined_card = credit_card('1111111111111111') @credit_card_d3d = credit_card('4000000000000002', verification_value: '111') + @credit_card_d3d_2_challenge = credit_card('5130257474533310', verification_value: '123') + @credit_card_d3d_2_frictionless = credit_card('4186455175836497', verification_value: '123') @options = { order_id: generate_unique_id[0...30], billing_address: address, @@ -17,23 +23,39 @@ def setup currency: fixtures(:ogone)[:currency] || 'EUR', origin: 'STORE' } + @options_browser_info = { + three_ds_2: { + browser_info: { + "width": 390, + "height": 400, + "depth": 24, + "timezone": 300, + "user_agent": 'Spreedly Agent', + "java": false, + "javascript": true, + "language": 'en-US', + "browser_size": '05', + "accept_header": 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' + } + } + } end def test_successful_purchase - assert response = @gateway.purchase(@amount, @credit_card, @options) + assert response = @gateway_3ds.purchase(@amount, @credit_card, @options) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message assert_equal @options[:order_id], response.order_id end def test_successful_purchase_with_utf8_encoding_1 - assert response = @gateway.purchase(@amount, credit_card('4000100011112224', first_name: 'Rémy', last_name: 'Fröåïør'), @options) + assert response = @gateway_3ds.purchase(@amount, credit_card('4000100011112224', first_name: 'Rémy', last_name: 'Fröåïør'), @options) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end def test_successful_purchase_with_utf8_encoding_2 - assert response = @gateway.purchase(@amount, credit_card('4000100011112224', first_name: 'ワタシ', last_name: 'ёжзийклмнопрсуфхцч'), @options) + assert response = @gateway_3ds.purchase(@amount, credit_card('4000100011112224', first_name: 'ワタシ', last_name: 'ёжзийклмнопрсуфхцч'), @options) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end @@ -68,30 +90,89 @@ def test_successful_purchase_with_signature_encryptor_to_sha512 end # NOTE: You have to contact Ogone to make sure your test account allow 3D Secure transactions before running this test - def test_successful_purchase_with_3d_secure - assert response = @gateway.purchase(@amount, @credit_card_d3d, @options.merge(d3d: true)) + def test_successful_purchase_with_3d_secure_v1 + assert response = @gateway_3ds.purchase(@amount, @credit_card_d3d, @options.merge(@options_browser_info, d3d: true)) assert_success response assert_equal '46', response.params['STATUS'] assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message assert response.params['HTML_ANSWER'] + assert Base64.decode64(response.params['HTML_ANSWER']) + end + + def test_successful_purchase_with_3d_secure_v2 + assert response = @gateway_3ds.purchase(@amount, @credit_card_d3d_2_challenge, @options_browser_info.merge(d3d: true)) + assert_success response + assert_equal '46', response.params['STATUS'] + assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message + assert response.params['HTML_ANSWER'] + assert Base64.decode64(response.params['HTML_ANSWER']) + end + + def test_successful_purchase_with_3d_secure_v2_flag_updated + options = @options_browser_info.merge(three_d_secure: { required: true }) + assert response = @gateway_3ds.purchase(@amount, @credit_card_d3d, options) + assert_success response + assert_equal '46', response.params['STATUS'] + assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message + assert response.params['HTML_ANSWER'] + assert Base64.decode64(response.params['HTML_ANSWER']) + end + + def test_successful_purchase_with_3d_secure_v2_frictionless + assert response = @gateway_3ds.purchase(@amount, @credit_card_d3d_2_frictionless, @options_browser_info.merge(d3d: true)) + assert_success response + assert_includes response.params, 'PAYID' + assert_equal '0', response.params['NCERROR'] + assert_equal '9', response.params['STATUS'] + assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message + end + + def test_successful_purchase_with_3d_secure_v2_recomended_parameters + options = @options.merge(@options_browser_info) + assert response = @gateway_3ds.authorize(@amount, @credit_card_d3d_2_challenge, options.merge(d3d: true)) + assert_success response + assert_equal '46', response.params['STATUS'] + assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message + assert response.params['HTML_ANSWER'] + assert Base64.decode64(response.params['HTML_ANSWER']) + end + + def test_successful_purchase_with_3d_secure_v2_optional_parameters + options = @options.merge(@options_browser_info).merge(mpi: { threeDSRequestorChallengeIndicator: '04' }) + assert response = @gateway_3ds.authorize(@amount, @credit_card_d3d_2_challenge, options.merge(d3d: true)) + assert_success response + assert_equal '46', response.params['STATUS'] + assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message + assert response.params['HTML_ANSWER'] + assert Base64.decode64(response.params['HTML_ANSWER']) + end + + def test_unsuccessful_purchase_with_3d_secure_v2 + @credit_card_d3d_2_challenge.number = '4419177274955460' + assert response = @gateway_3ds.purchase(@amount, @credit_card_d3d_2_challenge, @options_browser_info.merge(d3d: true)) + assert_failure response + assert_includes response.params, 'PAYID' + assert_equal response.params['NCERROR'], '40001134' + assert_equal response.params['STATUS'], '2' + assert_equal response.params['NCERRORPLUS'], 'Authentication failed. Please retry or cancel.' end def test_successful_with_non_numeric_order_id @options[:order_id] = "##{@options[:order_id][0...26]}.12" - assert response = @gateway.purchase(@amount, @credit_card, @options) + assert response = @gateway_3ds.purchase(@amount, @credit_card, @options) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end def test_successful_purchase_without_explicit_order_id @options.delete(:order_id) - assert response = @gateway.purchase(@amount, @credit_card, @options) + assert response = @gateway_3ds.purchase(@amount, @credit_card, @options) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end def test_successful_purchase_with_custom_eci - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(eci: 4)) + assert response = @gateway_3ds.purchase(@amount, @credit_card, @options.merge(eci: 4)) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end @@ -99,8 +180,7 @@ def test_successful_purchase_with_custom_eci # NOTE: You have to allow USD as a supported currency in the "Account"->"Currencies" # section of your account admin on https://secure.ogone.com/ncol/test/frame_ogone.asp before running this test def test_successful_purchase_with_custom_currency_at_the_gateway_level - gateway = OgoneGateway.new(fixtures(:ogone).merge(currency: 'USD')) - assert response = gateway.purchase(@amount, @credit_card) + assert response = @gateway_3ds.purchase(@amount, @credit_card) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end @@ -108,92 +188,90 @@ def test_successful_purchase_with_custom_currency_at_the_gateway_level # NOTE: You have to allow USD as a supported currency in the "Account"->"Currencies" # section of your account admin on https://secure.ogone.com/ncol/test/frame_ogone.asp before running this test def test_successful_purchase_with_custom_currency - gateway = OgoneGateway.new(fixtures(:ogone).merge(currency: 'EUR')) - assert response = gateway.purchase(@amount, @credit_card, @options.merge(currency: 'USD')) + assert response = @gateway_3ds.purchase(@amount, @credit_card, @options.merge(currency: 'USD')) assert_success response assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message end def test_unsuccessful_purchase - assert response = @gateway.purchase(@amount, @declined_card, @options) + assert response = @gateway_3ds.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'No brand', response.message + assert_equal 'No brand or invalid card number', response.message end def test_successful_authorize_with_mastercard - assert auth = @gateway.authorize(@amount, @mastercard, @options) + assert auth = @gateway_3ds.authorize(@amount, @mastercard, @options) assert_success auth assert_equal BarclaysEpdqExtraPlusGateway::SUCCESS_MESSAGE, auth.message end def test_authorize_and_capture - assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert auth = @gateway_3ds.authorize(@amount, @credit_card, @options) assert_success auth assert_equal OgoneGateway::SUCCESS_MESSAGE, auth.message assert auth.authorization - assert capture = @gateway.capture(@amount, auth.authorization) + assert capture = @gateway_3ds.capture(@amount, auth.authorization) assert_success capture end def test_authorize_and_capture_with_custom_eci - assert auth = @gateway.authorize(@amount, @credit_card, @options.merge(eci: 4)) + assert auth = @gateway_3ds.authorize(@amount, @credit_card, @options.merge(eci: 4)) assert_success auth assert_equal OgoneGateway::SUCCESS_MESSAGE, auth.message assert auth.authorization - assert capture = @gateway.capture(@amount, auth.authorization, @options) + assert capture = @gateway_3ds.capture(@amount, auth.authorization, @options) assert_success capture end def test_unsuccessful_capture - assert response = @gateway.capture(@amount, '') + assert response = @gateway_3ds.capture(@amount, '') assert_failure response - assert_equal 'No card no, no exp date, no brand', response.message + assert_equal 'No card no, no exp date, no brand or invalid card number', response.message end def test_successful_void - assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert auth = @gateway_3ds.authorize(@amount, @credit_card, @options) assert_success auth assert auth.authorization - assert void = @gateway.void(auth.authorization) + assert void = @gateway_3ds.void(auth.authorization) assert_equal OgoneGateway::SUCCESS_MESSAGE, auth.message assert_success void end def test_successful_store - assert response = @gateway.store(@credit_card, billing_id: 'test_alias') + assert response = @gateway_3ds.store(@credit_card, billing_id: 'test_alias') assert_success response - assert purchase = @gateway.purchase(@amount, 'test_alias') + assert purchase = @gateway_3ds.purchase(@amount, 'test_alias') assert_success purchase end def test_successful_store_with_store_amount_at_the_gateway_level - gateway = OgoneGateway.new(fixtures(:ogone).merge(store_amount: 100)) - assert response = gateway.store(@credit_card, billing_id: 'test_alias') + assert response = @gateway_3ds.store(@credit_card, billing_id: 'test_alias') assert_success response - assert purchase = gateway.purchase(@amount, 'test_alias') + assert purchase = @gateway_3ds.purchase(@amount, 'test_alias') assert_success purchase end def test_successful_store_generated_alias - assert response = @gateway.store(@credit_card) + assert response = @gateway_3ds.store(@credit_card) assert_success response - assert purchase = @gateway.purchase(@amount, response.billing_id) + assert purchase = @gateway_3ds.purchase(@amount, response.billing_id) assert_success purchase end def test_successful_refund - assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert purchase = @gateway_3ds.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount, purchase.authorization, @options) + assert refund = @gateway_3ds.refund(@amount, purchase.authorization, @options) assert_success refund assert refund.authorization assert_equal OgoneGateway::SUCCESS_MESSAGE, refund.message end def test_unsuccessful_refund - assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert purchase = @gateway_3ds.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount + 1, purchase.authorization, @options) # too much refund requested + assert refund = @gateway_3ds.refund(@amount + 1, purchase.authorization, @options) # too much refund requested assert_failure refund assert refund.authorization assert_equal 'Overflow in refunds requests', refund.message @@ -207,26 +285,26 @@ def test_successful_credit end def test_successful_verify - response = @gateway.verify(@credit_card, @options) + response = @gateway_3ds.verify(@credit_card, @options) assert_success response assert_equal 'The transaction was successful', response.message end def test_failed_verify - response = @gateway.verify(@declined_card, @options) + response = @gateway_3ds.verify(@declined_card, @options) assert_failure response - assert_equal 'No brand', response.message + assert_equal 'No brand or invalid card number', response.message end def test_reference_transactions # Setting an alias - assert response = @gateway.purchase(@amount, credit_card('4000100011112224'), @options.merge(billing_id: 'awesomeman', order_id: Time.now.to_i.to_s + '1')) + assert response = @gateway_3ds.purchase(@amount, credit_card('4000100011112224'), @options.merge(billing_id: 'awesomeman', order_id: Time.now.to_i.to_s + '1')) assert_success response # Updating an alias - assert response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(billing_id: 'awesomeman', order_id: Time.now.to_i.to_s + '2')) + assert response = @gateway_3ds.purchase(@amount, credit_card('4111111111111111'), @options.merge(billing_id: 'awesomeman', order_id: Time.now.to_i.to_s + '2')) assert_success response # Using an alias (i.e. don't provide the credit card) - assert response = @gateway.purchase(@amount, 'awesomeman', @options.merge(order_id: Time.now.to_i.to_s + '3')) + assert response = @gateway_3ds.purchase(@amount, 'awesomeman', @options.merge(order_id: Time.now.to_i.to_s + '3')) assert_success response end diff --git a/test/remote/gateways/remote_openpay_test.rb b/test/remote/gateways/remote_openpay_test.rb index 0bab989b01f..b7d336d78d6 100644 --- a/test/remote/gateways/remote_openpay_test.rb +++ b/test/remote/gateways/remote_openpay_test.rb @@ -16,6 +16,19 @@ def setup end def test_successful_purchase + @options[:email] = '%d@example.org' % Time.now + @options[:name] = 'Customer name' + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_nil response.message + end + + def test_successful_purchase_with_mexico_url + @options[:email] = '%d@example.org' % Time.now + @options[:name] = 'Customer name' + credentials = fixtures(:openpay).merge(merchant_country: 'MX') + @gateway = OpenpayGateway.new(credentials) + assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_nil response.message @@ -31,7 +44,7 @@ def test_successful_purchase_with_email def test_unsuccessful_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'The card was declined', response.message + assert_equal 'The card was declined by the bank', response.message end def test_successful_refund @@ -69,7 +82,7 @@ def test_successful_authorize_with_email def test_unsuccessful_authorize assert response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_equal 'The card was declined', response.message + assert_equal 'The card was declined by the bank', response.message end def test_successful_capture diff --git a/test/remote/gateways/remote_orbital_test.rb b/test/remote/gateways/remote_orbital_test.rb index c9a1ce968e1..398e33b53c4 100644 --- a/test/remote/gateways/remote_orbital_test.rb +++ b/test/remote/gateways/remote_orbital_test.rb @@ -195,11 +195,13 @@ def test_successful_purchase_with_visa_network_tokenization_credit_card_with_eci end def test_successful_purchase_with_master_card_network_tokenization_credit_card - network_card = network_tokenization_credit_card('4788250000028291', + network_card = network_tokenization_credit_card( + '4788250000028291', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', verification_value: '111', - brand: 'master') + brand: 'master' + ) assert response = @gateway.purchase(3000, network_card, @options) assert_success response assert_equal 'Approved', response.message @@ -249,11 +251,13 @@ def test_successful_purchase_with_sca_merchant_initiated_master_card end def test_successful_purchase_with_american_express_network_tokenization_credit_card - network_card = network_tokenization_credit_card('4788250000028291', + network_card = network_tokenization_credit_card( + '4788250000028291', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', verification_value: '111', - brand: 'american_express') + brand: 'american_express' + ) assert response = @gateway.purchase(3000, network_card, @options) assert_success response assert_equal 'Approved', response.message @@ -261,11 +265,13 @@ def test_successful_purchase_with_american_express_network_tokenization_credit_c end def test_successful_purchase_with_discover_network_tokenization_credit_card - network_card = network_tokenization_credit_card('4788250000028291', + network_card = network_tokenization_credit_card( + '4788250000028291', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', verification_value: '111', - brand: 'discover') + brand: 'discover' + ) assert response = @gateway.purchase(3000, network_card, @options) assert_success response assert_equal 'Approved', response.message @@ -349,6 +355,15 @@ def test_successful_purchase_with_echeck_on_next_day assert_false response.authorization.blank? end + def test_successful_purchase_with_commercial_echeck + commercial_echeck = check(account_number: '072403004', account_type: 'checking', account_holder_type: 'business', routing_number: '072403004') + + assert response = @echeck_gateway.purchase(20, commercial_echeck, @options) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + def test_successful_purchase_with_mit_stored_credentials mit_stored_credentials = { mit_msg_type: 'MUSE', @@ -565,6 +580,16 @@ def test_successful_refund assert_success refund end + def test_successful_refund_with_payment_source + amount = @amount + assert response = @gateway.purchase(amount, @credit_card, @options) + assert_success response + assert response.authorization + + assert refund = @gateway.refund(amount, '', @options.merge({ payment_method: @credit_card })) + assert_success refund + end + def test_failed_refund assert refund = @gateway.refund(@amount, '123;123', @options) assert_failure refund @@ -964,6 +989,26 @@ def setup zip: '03105', country: 'US' } + }, + discover: { + card: { + number: '6011016011016011', + verification_value: '613', + brand: 'discover' + }, + three_d_secure: { + eci: '6', + cavv: 'Asju1ljfl86bAAAAAACm9zU6aqY=', + ds_transaction_id: '32b274ee-582d-4232-b20a-363f2acafa5a' + }, + address: { + address1: '1 Northeastern Blvd', + address2: '', + city: 'Bedford', + state: 'NH', + zip: '03109', + country: 'US' + } } } end @@ -1016,6 +1061,22 @@ def test_successful_3ds_purchase_with_american_express assert_success_with_authorization(response) end + def test_successful_3ds_authorization_with_discover + cc = brand_specific_card(@brand_specific_fixtures[:discover][:card]) + options = brand_specific_3ds_options(@brand_specific_fixtures[:discover]) + + assert response = @three_ds_gateway.authorize(100, cc, options) + assert_success_with_authorization(response) + end + + def test_successful_3ds_purchase_with_discover + cc = brand_specific_card(@brand_specific_fixtures[:discover][:card]) + options = brand_specific_3ds_options(@brand_specific_fixtures[:discover]) + + assert response = @three_ds_gateway.purchase(100, cc, options) + assert_success_with_authorization(response) + end + private def assert_success_with_authorization(response) @@ -1171,11 +1232,13 @@ def test_successful_purchase_with_visa_network_tokenization_credit_card_with_eci end def test_successful_purchase_with_master_card_network_tokenization_credit_card - network_card = network_tokenization_credit_card('4788250000028291', + network_card = network_tokenization_credit_card( + '4788250000028291', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', verification_value: '111', - brand: 'master') + brand: 'master' + ) assert response = @tandem_gateway.purchase(3000, network_card, @options) assert_success response assert_equal 'Approved', response.message @@ -1183,11 +1246,13 @@ def test_successful_purchase_with_master_card_network_tokenization_credit_card end def test_successful_purchase_with_american_express_network_tokenization_credit_card - network_card = network_tokenization_credit_card('4788250000028291', + network_card = network_tokenization_credit_card( + '4788250000028291', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', verification_value: '111', - brand: 'american_express') + brand: 'american_express' + ) assert response = @tandem_gateway.purchase(3000, network_card, @options) assert_success response assert_equal 'Approved', response.message @@ -1195,11 +1260,13 @@ def test_successful_purchase_with_american_express_network_tokenization_credit_c end def test_successful_purchase_with_discover_network_tokenization_credit_card - network_card = network_tokenization_credit_card('4788250000028291', + network_card = network_tokenization_credit_card( + '4788250000028291', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', verification_value: '111', - brand: 'discover') + brand: 'discover' + ) assert response = @tandem_gateway.purchase(3000, network_card, @options) assert_success response assert_equal 'Approved', response.message diff --git a/test/remote/gateways/remote_pay_junction_test.rb b/test/remote/gateways/remote_pay_junction_test.rb index b5a0292cd25..280e89e3883 100644 --- a/test/remote/gateways/remote_pay_junction_test.rb +++ b/test/remote/gateways/remote_pay_junction_test.rb @@ -71,8 +71,7 @@ def test_successful_capture response = @gateway.capture(AMOUNT, auth.authorization, @options) assert_success response assert_equal 'capture', response.params['posture'], 'Should be a capture' - assert_equal auth.authorization, response.authorization, - 'Should maintain transaction ID across request' + assert_equal auth.authorization, response.authorization, 'Should maintain transaction ID across request' end def test_successful_credit @@ -93,8 +92,7 @@ def test_successful_void assert response = @gateway.void(purchase.authorization, order_id: order_id) assert_success response assert_equal 'void', response.params['posture'], 'Should be a capture' - assert_equal purchase.authorization, response.authorization, - 'Should maintain transaction ID across request' + assert_equal purchase.authorization, response.authorization, 'Should maintain transaction ID across request' end def test_successful_instant_purchase @@ -110,17 +108,19 @@ def test_successful_instant_purchase assert_equal PayJunctionGateway::SUCCESS_MESSAGE, response.message assert_equal 'capture', response.params['posture'], 'Should be captured funds' assert_equal 'charge', response.params['transaction_action'] - assert_not_equal purchase.authorization, response.authorization, - 'Should have recieved new transaction ID' + assert_not_equal purchase.authorization, response.authorization, 'Should have recieved new transaction ID' assert_success response end def test_successful_recurring - assert response = @gateway.recurring(AMOUNT, @credit_card, + assert response = @gateway.recurring( + AMOUNT, + @credit_card, periodicity: :monthly, payments: 12, - order_id: generate_unique_id[0..15]) + order_id: generate_unique_id[0..15] + ) assert_equal PayJunctionGateway::SUCCESS_MESSAGE, response.message assert_equal 'charge', response.params['transaction_action'] diff --git a/test/remote/gateways/remote_pay_trace_test.rb b/test/remote/gateways/remote_pay_trace_test.rb index b05922897d6..b10e5119e3a 100644 --- a/test/remote/gateways/remote_pay_trace_test.rb +++ b/test/remote/gateways/remote_pay_trace_test.rb @@ -22,6 +22,7 @@ def setup @invalid_card = credit_card('54545454545454', month: '14', year: '1999') @discover = credit_card('6011000993026909') @amex = credit_card('371449635392376') + @echeck = check(account_number: '123456', routing_number: '325070760') @options = { billing_address: { address1: '8320 This Way Lane', @@ -52,6 +53,22 @@ def test_successful_purchase_with_customer_id assert_equal 'Your transaction was successfully approved.', response.message end + def test_successful_purchase_with_ach + @echeck.account_number = rand.to_s[2..7] + response = @gateway.purchase(1000, @echeck, @options) + assert_success response + assert_equal response.message, 'Your check was successfully processed.' + end + + def test_successful_purchase_by_customer_with_ach + @echeck.account_number = rand.to_s[2..7] + create = @gateway.store(@echeck, @options) + assert_success create + customer_id = create.params['customer_id'] + response = @gateway.purchase(500, customer_id, @options.merge({ check_transaction: 'true' })) + assert_success response + end + def test_successful_purchase_with_more_options options = { email: 'joe@example.com' @@ -192,6 +209,23 @@ def test_successful_authorize_with_customer_id assert_equal 'Your transaction was successfully approved.', response.message end + def test_successful_authorize_with_ach + @echeck.account_number = rand.to_s[2..7] + response = @gateway.authorize(1000, @echeck, @options) + assert_success response + assert_equal response.message, 'Your check was successfully processed.' + end + + def test_successful_authorize_by_customer_with_ach + @echeck.account_number = rand.to_s[2..7] + store = @gateway.store(@echeck, @options) + assert_success store + customer_id = store.params['customer_id'] + + response = @gateway.authorize(200, customer_id, @options.merge({ check_transaction: 'true' })) + assert_success response + end + def test_successful_authorize_and_capture_with_level_3_data options = { visa_or_mastercard: 'mastercard', @@ -237,6 +271,15 @@ def test_partial_capture assert_success capture end + def test_authorize_and_capture_with_ach + @echeck.account_number = rand.to_s[2..7] + auth = @gateway.authorize(500, @echeck, @options) + assert_success auth + + assert capture = @gateway.capture(500, auth.authorization, @options.merge({ check_transaction: 'true' })) + assert_success capture + end + def test_failed_capture response = @gateway.capture(@amount, '') assert_failure response @@ -301,6 +344,15 @@ def test_successful_void assert_equal 'Your transaction was successfully voided.', void.message end + def test_successful_void_with_ach + @echeck.account_number = rand.to_s[2..7] + auth = @gateway.authorize(@amount, @echeck, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization, { check_transaction: 'true' }) + assert_success void + end + def test_failed_void response = @gateway.void('') assert_failure response diff --git a/test/remote/gateways/remote_payeezy_test.rb b/test/remote/gateways/remote_payeezy_test.rb index 27659ba5130..177511fa45c 100644 --- a/test/remote/gateways/remote_payeezy_test.rb +++ b/test/remote/gateways/remote_payeezy_test.rb @@ -41,6 +41,25 @@ def setup initiator: 'cardholder' } } + @apple_pay_card = network_tokenization_credit_card( + '4761209980011439', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: Time.now.year + 1, + eci: 5, + source: :apple_pay, + verification_value: 569 + ) + @apple_pay_card_amex = network_tokenization_credit_card( + '373953192351004', + brand: 'american_express', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: Time.now.year + 1, + eci: 5, + source: :apple_pay, + verification_value: 569 + ) end def test_successful_store @@ -72,6 +91,24 @@ def test_successful_purchase assert_success response end + def test_successful_purchase_with_apple_pay + assert response = @gateway.purchase(@amount, @apple_pay_card, @options) + assert_success response + end + + def test_successful_purchase_with_apple_pay_amex + assert response = @gateway.purchase(@amount, @apple_pay_card_amex, @options) + assert_success response + end + + def test_successful_authorize_and_capture_with_apple_pay + assert auth = @gateway.authorize(@amount, @apple_pay_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + end + def test_successful_purchase_with_echeck options = @options.merge({ customer_id_type: '1', customer_id_number: '1', client_email: 'test@example.com' }) assert response = @gateway.purchase(@amount, @check, options) @@ -103,6 +140,23 @@ def test_successful_purchase_with_standardized_stored_credentials assert_success response end + def test_successful_purchase_with_apple_pay_name_from_billing_address + @apple_pay_card.first_name = nil + @apple_pay_card.last_name = nil + assert response = @gateway.purchase(@amount, @apple_pay_card, @options) + assert_success response + assert_equal 'Jim Smith', response.params['card']['cardholder_name'] + end + + def test_failed_purchase_with_apple_pay_no_name + @options[:billing_address] = nil + @apple_pay_card.first_name = nil + @apple_pay_card.last_name = nil + assert response = @gateway.purchase(@amount, @apple_pay_card, @options) + assert_failure response + assert_equal 'Bad Request (27) - Invalid Card Holder', response.message + end + def test_failed_purchase @amount = 501300 assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -117,6 +171,48 @@ def test_failed_purchase_with_insufficient_funds assert_match(/Insufficient Funds/, response.message) end + def test_successful_purchase_with_three_ds_data + @options[:three_d_secure] = { + version: '1', + eci: '05', + cavv: '3q2+78r+ur7erb7vyv66vv////8=', + acs_transaction_id: '6546464645623455665165+qe-jmhabcdefg' + } + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_match(/Transaction Normal/, response.message) + assert_equal '100', response.params['bank_resp_code'] + assert_equal nil, response.error_code + assert_success response + end + + def test_authorize_and_capture_three_ds_data + @options[:three_d_secure] = { + version: '1', + eci: '05', + cavv: '3q2+78r+ur7erb7vyv66vv////8=', + acs_transaction_id: '6546464645623455665165+qe-jmhabcdefg' + } + assert auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + assert auth.authorization + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + end + + def test_purchase_with_three_ds_version_data + @options[:three_d_secure] = { + version: '1.0.2', + eci: '05', + cavv: '3q2+78r+ur7erb7vyv66vv////8=', + acs_transaction_id: '6546464645623455665165+qe-jmhabcdefg' + } + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_match(/Transaction Normal/, response.message) + assert_equal '100', response.params['bank_resp_code'] + assert_equal nil, response.error_code + assert_success response + end + def test_authorize_and_capture assert auth = @gateway.authorize(@amount, @credit_card, @options) assert_success auth @@ -394,4 +490,14 @@ def test_transcript_scrubbing_echeck assert_scrubbed(@check.routing_number, transcript) assert_scrubbed(@gateway.options[:token], transcript) end + + def test_transcript_scrubbing_network_token + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @apple_pay_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@apple_pay_card.payment_cryptogram, transcript) + assert_scrubbed(@apple_pay_card.verification_value, transcript) + end end diff --git a/test/remote/gateways/remote_payflow_test.rb b/test/remote/gateways/remote_payflow_test.rb index 71823d5d617..ed12025cd79 100644 --- a/test/remote/gateways/remote_payflow_test.rb +++ b/test/remote/gateways/remote_payflow_test.rb @@ -445,12 +445,15 @@ def test_reference_purchase def test_recurring_with_initial_authorization response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(1000, @credit_card, + @gateway.recurring( + 1000, + @credit_card, periodicity: :monthly, initial_transaction: { type: :purchase, amount: 500 - }) + } + ) end assert_success response diff --git a/test/remote/gateways/remote_paymentez_test.rb b/test/remote/gateways/remote_paymentez_test.rb index 37d3fced73c..910b16c262d 100644 --- a/test/remote/gateways/remote_paymentez_test.rb +++ b/test/remote/gateways/remote_paymentez_test.rb @@ -8,13 +8,15 @@ def setup @amount = 100 @credit_card = credit_card('4111111111111111', verification_value: '666') @otp_card = credit_card('36417002140808', verification_value: '666') - @elo_credit_card = credit_card('6362970000457013', + @elo_credit_card = credit_card( + '6362970000457013', month: 10, year: 2022, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'elo') + brand: 'elo' + ) @declined_card = credit_card('4242424242424242', verification_value: '666') @options = { billing_address: address, @@ -303,6 +305,15 @@ def test_unstore_with_elo assert_success response end + def test_successful_inquire_with_transaction_id + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + gateway_transaction_id = response.authorization + response = @gateway.inquire(gateway_transaction_id, @options) + assert_success response + end + def test_invalid_login gateway = PaymentezGateway.new(application_code: '9z8y7w6x', app_key: '1a2b3c4d') diff --git a/test/remote/gateways/remote_paypal_test.rb b/test/remote/gateways/remote_paypal_test.rb index d73c21528c8..eee5d7aba1a 100644 --- a/test/remote/gateways/remote_paypal_test.rb +++ b/test/remote/gateways/remote_paypal_test.rb @@ -232,9 +232,11 @@ def test_successful_multiple_transfer response = @gateway.purchase(900, @credit_card, @params) assert_success response - response = @gateway.transfer([@amount, 'joe@example.com'], + response = @gateway.transfer( + [@amount, 'joe@example.com'], [600, 'jane@example.com', { note: 'Thanks for taking care of that' }], - subject: 'Your money') + subject: 'Your money' + ) assert_success response end diff --git a/test/remote/gateways/remote_paysafe_test.rb b/test/remote/gateways/remote_paysafe_test.rb index 70305491741..72f5c0a3aae 100644 --- a/test/remote/gateways/remote_paysafe_test.rb +++ b/test/remote/gateways/remote_paysafe_test.rb @@ -148,6 +148,20 @@ def test_successful_purchase_with_airline_details assert_equal 'F', response.params['airlineTravelDetails']['tripLegs']['leg2']['serviceClass'] end + def test_successful_purchase_with_truncated_address + options = { + billing_address: { + address1: "This is an extremely long address, it is unreasonably long and we can't allow it.", + address2: "This is an extremely long address2, it is unreasonably long and we can't allow it.", + city: 'Lake Chargoggagoggmanchauggagoggchaubunagungamaugg', + state: 'NC', + zip: '27701' + } + } + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + def test_successful_purchase_with_token response = @gateway.purchase(200, @pm_token, @options) assert_success response @@ -216,6 +230,22 @@ def test_successful_purchase_with_stored_credentials assert_equal 'COMPLETED', response.message end + # Merchant account must be setup to support funding transaction, and funding transaction type must be correct for the MCC + def test_successful_purchase_with_correct_funding_transaction_type + response = @gateway.purchase(@amount, @credit_card, @options.merge({ funding_transaction: 'SDW_WALLET_TRANSFER' })) + assert_success response + assert_equal 'COMPLETED', response.message + assert_equal 0, response.params['availableToSettle'] + assert_not_nil response.params['authCode'] + assert_match 'SDW_WALLET_TRANSFER', response.params['fundingTransaction']['type'] + end + + def test_failed_purchase_with_incorrect_funding_transaction_type + response = @gateway.purchase(@amount, @credit_card, @options.merge({ funding_transaction: 'SVDW_FUNDS_TRANSFER' })) + assert_failure response + assert_equal 'Error(s)- code:3068, message:You submitted a funding transaction that is not correct for the merchant account.', response.message + end + def test_failed_purchase response = @gateway.purchase(11, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_payu_latam_test.rb b/test/remote/gateways/remote_payu_latam_test.rb index 88860749ea7..b84990a4422 100644 --- a/test/remote/gateways/remote_payu_latam_test.rb +++ b/test/remote/gateways/remote_payu_latam_test.rb @@ -346,7 +346,7 @@ def test_successful_partial_capture def test_failed_capture response = @gateway.capture(@amount, '') assert_failure response - assert_match(/must not be null/, response.message) + assert_match(/may not be null/, response.message) end def test_successful_refund @@ -414,14 +414,14 @@ def test_failed_verify_with_specified_amount verify = @gateway.verify(@credit_card, @options.merge(verify_amount: 0)) assert_failure verify - assert_equal 'The amount must be greater than zero', verify.message + assert_equal 'INVALID_TRANSACTION | [The given payment value [0] is inferior than minimum configured value [0.01]]', verify.message end def test_failed_verify_with_specified_language verify = @gateway.verify(@credit_card, @options.merge(verify_amount: 0, language: 'es')) assert_failure verify - assert_equal 'El valor de la transacción debe ser mayor a cero', verify.message + assert_equal 'INVALID_TRANSACTION | [El valor recibido [0] es inferior al valor mínimo configurado [0,01]]', verify.message end def test_transcript_scrubbing @@ -440,4 +440,10 @@ def test_successful_store assert_success store assert_equal 'SUCCESS', store.message end + + def test_successful_purchase_with_extra_fields + response = @gateway.purchase(@amount, @credit_card, @options.merge({ extra_1: '123456', extra_2: 'abcdef', extra_3: 'testing' })) + assert_success response + assert_equal 'APPROVED', response.message + end end diff --git a/test/remote/gateways/remote_payway_test.rb b/test/remote/gateways/remote_payway_test.rb index 5276e60e38e..643411f520a 100644 --- a/test/remote/gateways/remote_payway_test.rb +++ b/test/remote/gateways/remote_payway_test.rb @@ -8,37 +8,49 @@ def setup @gateway = ActiveMerchant::Billing::PaywayGateway.new(fixtures(:payway)) - @visa = credit_card('4564710000000004', + @visa = credit_card( + '4564710000000004', month: 2, year: 2019, - verification_value: '847') + verification_value: '847' + ) - @mastercard = credit_card('5163200000000008', + @mastercard = credit_card( + '5163200000000008', month: 8, year: 2020, verification_value: '070', - brand: 'master') + brand: 'master' + ) - @expired = credit_card('4564710000000012', + @expired = credit_card( + '4564710000000012', month: 2, year: 2005, - verification_value: '963') + verification_value: '963' + ) - @low = credit_card('4564710000000020', + @low = credit_card( + '4564710000000020', month: 5, year: 2020, - verification_value: '234') + verification_value: '234' + ) - @stolen_mastercard = credit_card('5163200000000016', + @stolen_mastercard = credit_card( + '5163200000000016', month: 12, year: 2019, verification_value: '728', - brand: 'master') + brand: 'master' + ) - @invalid = credit_card('4564720000000037', + @invalid = credit_card( + '4564720000000037', month: 9, year: 2019, - verification_value: '030') + verification_value: '030' + ) end def test_successful_visa diff --git a/test/remote/gateways/remote_plexo_test.rb b/test/remote/gateways/remote_plexo_test.rb new file mode 100644 index 00000000000..433a8a72245 --- /dev/null +++ b/test/remote/gateways/remote_plexo_test.rb @@ -0,0 +1,266 @@ +require 'test_helper' + +class RemotePlexoTest < Test::Unit::TestCase + def setup + @gateway = PlexoGateway.new(fixtures(:plexo)) + + @amount = 100 + @credit_card = credit_card('5555555555554444', month: '12', year: '2024', verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + @declined_card = credit_card('5555555555554445') + @options = { + email: 'snavatta@plexo.com.uy', + ip: '127.0.0.1', + items: [ + { + name: 'prueba', + description: 'prueba desc', + quantity: '1', + price: '100', + discount: '0' + } + ], + amount_details: { + tip_amount: '5' + }, + identification_type: '1', + identification_value: '123456', + billing_address: address + } + + @cancel_options = { + description: 'Test desc', + reason: 'requested by client' + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + end + + def test_successful_purchase_with_finger_print + response = @gateway.purchase(@amount, @credit_card, @options.merge({ finger_print: 'USABJHABSFASNJKN123532' })) + assert_success response + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_equal 'denied', response.params['status'] + assert_equal '10', response.error_code + end + + def test_successful_authorize_with_metadata + meta = { + custom_one: 'my field 1' + } + auth = @gateway.authorize(@amount, @credit_card, @options.merge({ metadata: meta })) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + end + + def test_successful_authorize_and_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_equal '10', response.error_code + assert_equal 'denied', response.params['status'] + end + + def test_partial_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount - 1, auth.authorization) + assert_success capture + end + + def test_failed_capture + response = @gateway.capture(@amount, '123') + assert_failure response + assert_equal 'An internal error occurred. Contact support.', response.message + end + + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @cancel_options) + assert_success refund + end + + def test_partial_refund + purchase = @gateway.purchase(@amount, @credit_card, @options.merge({ refund_type: 'partial-refund' })) + assert_success purchase + + assert refund = @gateway.refund(@amount - 1, purchase.authorization, @cancel_options.merge({ type: 'partial-refund' })) + assert_success refund + end + + def test_failed_refund + response = @gateway.refund(@amount, '123', @cancel_options) + assert_failure response + assert_equal 'An internal error occurred. Contact support.', response.message + end + + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization, @cancel_options) + assert_success void + end + + def test_failed_void + response = @gateway.void('123', @cancel_options) + assert_failure response + assert_equal 'An internal error occurred. Contact support.', response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + end + + def test_successful_verify_with_custom_amount + response = @gateway.verify(@credit_card, @options.merge({ verify_amount: '400' })) + assert_success response + end + + def test_failed_verify + response = @gateway.verify(@declined_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_invalid_login + gateway = PlexoGateway.new(client_id: '', api_key: '') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@gateway.options[:api_key], transcript) + end + + def test_successful_purchase_passcard + credit_card = credit_card('6280260025383009', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + end + + def test_successful_purchase_edenred + credit_card = credit_card('6374830000000823', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + end + + def test_successful_purchase_anda + credit_card = credit_card('6031991248204901', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + end + + # This test is omitted until Plexo confirms that the transaction will indeed + # be declined as indicated in the documentation. + def test_successful_purchase_and_declined_refund_anda + omit + credit_card = credit_card('6031997614492616', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + purchase = @gateway.purchase(@amount, credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @cancel_options) + assert_failure refund + assert_equal 'An internal error occurred. Contact support.', refund.message + end + + # This test is omitted until Plexo confirms that the transaction will indeed + # be declined as indicated in the documentation. + def test_successful_purchase_and_declined_cancellation_anda + omit + credit_card = credit_card('6031998427187914', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + purchase = @gateway.purchase(@amount, credit_card, @options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization, @cancel_options) + assert_failure void + end + + def test_successful_purchase_tarjetad + credit_card = credit_card('6018287227431046', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + end + + def test_failure_purchase_tarjetad + credit_card = credit_card('6018282227431033', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + response = @gateway.purchase(@amount, credit_card, @options) + assert_failure response + assert_equal 'denied', response.params['status'] + assert_equal '10', response.error_code + end + + def test_successful_purchase_sodexo + credit_card = credit_card('5058645584812145', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + end + + # This test is omitted until Plexo confirms that the transaction will indeed + # be declined as indicated in the documentation. + def test_successful_purchase_and_declined_refund_sodexo + omit + credit_card = credit_card('5058647731868699', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + purchase = @gateway.purchase(@amount, credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, @cancel_options) + assert_failure refund + assert_equal 'An internal error occurred. Contact support.', refund.message + end + + def test_successful_purchase_and_declined_cancellation_sodexo + credit_card = credit_card('5058646599260130', month: '12', year: '2024', + verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + + purchase = @gateway.purchase(@amount, credit_card, @options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization, @cancel_options) + assert_success void + end +end diff --git a/test/remote/gateways/remote_priority_test.rb b/test/remote/gateways/remote_priority_test.rb index a3497003ba4..ef3849fb283 100644 --- a/test/remote/gateways/remote_priority_test.rb +++ b/test/remote/gateways/remote_priority_test.rb @@ -5,6 +5,7 @@ def setup @gateway = PriorityGateway.new(fixtures(:priority)) @amount = 2 + @credit_amount = 2000 @credit_card = credit_card @invalid_credit_card = credit_card('123456') @replay_id = rand(100...99999999) @@ -18,6 +19,20 @@ def setup tax_exempt: true } + @additional_creditoptions = { + is_auth: true, + should_get_credit_card_level: false, + should_vault_card: false, + invoice: '123', + tax_exempt: true, + is_ticket: false, + source_zip: '30022', + auth_code: '', + ach_indicator: '', + bank_account: '', + meta: 'Harry Maguire is the best defender in premier league' + } + @custom_pos_data = { pos_data: { cardholder_presence: 'NotPresent', @@ -147,6 +162,29 @@ def test_successful_purchase_with_additional_options assert_equal 'Approved or completed successfully', response.message end + def test_successful_credit + options = @options.merge(@additional_creditoptions) + response = @gateway.credit(@credit_amount, @credit_card, options) + assert_success response + assert_equal 'Approved or completed successfully', response.message + end + + def test_failed_credit + options = @options.merge(@additional_creditoptions) + response = @gateway.credit(@credit_amount, @invalid_credit_card, options) + assert_failure response + assert_equal 'Invalid card number', response.message + end + + def test_failed_credit_missing_card_month + card_without_month = credit_card('4242424242424242', month: '') + options = @options.merge(@additional_creditoptions) + response = @gateway.credit(@credit_amount, card_without_month, options) + assert_failure response + assert_equal 'ValidationError', response.error_code + assert_equal 'Missing expiration month and / or year', response.message + end + def test_successful_void_with_batch_open purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase diff --git a/test/remote/gateways/remote_psl_card_test.rb b/test/remote/gateways/remote_psl_card_test.rb index 44630800f89..18e911faea2 100644 --- a/test/remote/gateways/remote_psl_card_test.rb +++ b/test/remote/gateways/remote_psl_card_test.rb @@ -24,65 +24,55 @@ def setup end def test_successful_visa_purchase - response = @gateway.purchase(@accept_amount, @visa, - billing_address: @visa_address) + response = @gateway.purchase(@accept_amount, @visa, billing_address: @visa_address) assert_success response assert response.test? end def test_successful_visa_debit_purchase - response = @gateway.purchase(@accept_amount, @visa_debit, - billing_address: @visa_debit_address) + response = @gateway.purchase(@accept_amount, @visa_debit, billing_address: @visa_debit_address) assert_success response end # Fix regression discovered in production def test_visa_debit_purchase_should_not_send_debit_info_if_present @visa_debit.start_month = '07' - response = @gateway.purchase(@accept_amount, @visa_debit, - billing_address: @visa_debit_address) + response = @gateway.purchase(@accept_amount, @visa_debit, billing_address: @visa_debit_address) assert_success response end def test_successful_visa_purchase_specifying_currency - response = @gateway.purchase(@accept_amount, @visa, - billing_address: @visa_address, - currency: 'GBP') + response = @gateway.purchase(@accept_amount, @visa, billing_address: @visa_address, currency: 'GBP') assert_success response assert response.test? end def test_successful_solo_purchase - response = @gateway.purchase(@accept_amount, @solo, - billing_address: @solo_address) + response = @gateway.purchase(@accept_amount, @solo, billing_address: @solo_address) assert_success response assert response.test? end def test_referred_purchase - response = @gateway.purchase(@referred_amount, @uk_maestro, - billing_address: @uk_maestro_address) + response = @gateway.purchase(@referred_amount, @uk_maestro, billing_address: @uk_maestro_address) assert_failure response assert response.test? end def test_declined_purchase - response = @gateway.purchase(@declined_amount, @uk_maestro, - billing_address: @uk_maestro_address) + response = @gateway.purchase(@declined_amount, @uk_maestro, billing_address: @uk_maestro_address) assert_failure response assert response.test? end def test_declined_keep_card_purchase - response = @gateway.purchase(@keep_card_amount, @uk_maestro, - billing_address: @uk_maestro_address) + response = @gateway.purchase(@keep_card_amount, @uk_maestro, billing_address: @uk_maestro_address) assert_failure response assert response.test? end def test_successful_authorization - response = @gateway.authorize(@accept_amount, @visa, - billing_address: @visa_address) + response = @gateway.authorize(@accept_amount, @visa, billing_address: @visa_address) assert_success response assert response.test? end @@ -91,15 +81,13 @@ def test_no_login @gateway = PslCardGateway.new( login: '' ) - response = @gateway.authorize(@accept_amount, @uk_maestro, - billing_address: @uk_maestro_address) + response = @gateway.authorize(@accept_amount, @uk_maestro, billing_address: @uk_maestro_address) assert_failure response assert response.test? end def test_successful_authorization_and_capture - authorization = @gateway.authorize(@accept_amount, @visa, - billing_address: @visa_address) + authorization = @gateway.authorize(@accept_amount, @visa, billing_address: @visa_address) assert_success authorization assert authorization.test? diff --git a/test/remote/gateways/remote_rapyd_test.rb b/test/remote/gateways/remote_rapyd_test.rb index f352bb1d666..7451934b175 100644 --- a/test/remote/gateways/remote_rapyd_test.rb +++ b/test/remote/gateways/remote_rapyd_test.rb @@ -9,14 +9,34 @@ def setup @declined_card = credit_card('4111111111111105') @check = check @options = { - pm_type: 'us_visa_card', - currency: 'USD' + pm_type: 'us_debit_visa_card', + currency: 'USD', + complete_payment_url: 'www.google.com', + error_payment_url: 'www.google.com', + description: 'Describe this transaction', + statement_descriptor: 'Statement Descriptor', + email: 'test@example.com', + billing_address: address(name: 'Jim Reynolds'), + order_id: '987654321' + } + @stored_credential_options = { + pm_type: 'gb_visa_card', + currency: 'GBP', + complete_payment_url: 'https://www.rapyd.net/platform/collect/online/', + error_payment_url: 'https://www.rapyd.net/platform/collect/online/', + description: 'Describe this transaction', + statement_descriptor: 'Statement Descriptor', + email: 'test@example.com', + billing_address: address(name: 'Jim Reynolds'), + order_id: '987654321' } @ach_options = { pm_type: 'us_ach_bank', currency: 'USD', proof_of_authorization: false, - payment_purpose: 'Testing Purpose' + payment_purpose: 'Testing Purpose', + email: 'test@example.com', + billing_address: address(name: 'Jim Reynolds') } @metadata = { 'array_of_objects': [ @@ -34,6 +54,14 @@ def setup 'string': 'preferred', 'Boolean': true } + @three_d_secure = { + version: '2.1.0', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=', + xid: '00000000000000000501', + eci: '02' + } + + @address_object = address(line_1: '123 State Street', line_2: 'Apt. 34', zip: '12345', name: 'john doe', phone_number: '12125559999') end def test_successful_purchase @@ -42,15 +70,71 @@ def test_successful_purchase assert_equal 'SUCCESS', response.message end + def test_successful_authorize_with_mastercard + @options[:pm_type] = 'us_debit_mastercard_card' + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_mastercard + @options[:pm_type] = 'us_debit_mastercard_card' + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_success_purchase_without_address_object_customer + @options[:pm_type] = 'us_debit_discover_card' + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_subsequent_purchase_with_stored_credential + # Rapyd requires a random int between 10 and 15 digits for NTID + response = @gateway.purchase(15000, @credit_card, @stored_credential_options.merge(stored_credential: { network_transaction_id: rand.to_s[2..11], reason_type: 'recurring' })) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_network_transaction_id_and_initiation_type_fields + # Rapyd requires a random int between 10 and 15 digits for NTID + response = @gateway.purchase(15000, @credit_card, @stored_credential_options.merge(network_transaction_id: rand.to_s[2..11], initiation_type: 'customer_present')) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_network_transaction_id_and_initiation_type_fields_along_with_stored_credentials + # Rapyd requires a random int between 10 and 15 digits for NTID + response = @gateway.purchase(15000, @credit_card, @stored_credential_options.merge(stored_credential: { network_transaction_id: rand.to_s[2..11], reason_type: 'recurring' }, network_transaction_id: rand.to_s[2..11], initiation_type: 'customer_present')) + assert_success response + assert_equal 'SUCCESS', response.message + assert_equal 'customer_present', response.params['data']['initiation_type'] + end + + def test_successful_purchase_with_reccurence_type + response = @gateway.purchase(@amount, @credit_card, @options.merge(recurrence_type: 'recurring')) + assert_success response + assert_equal 'SUCCESS', response.message + end + def test_successful_purchase_with_address - options = { - address: { - name: 'Henry Winkler', - address1: '123 Happy Days Lane' - } - } + billing_address = address(name: 'Henry Winkler', address1: '123 Happy Days Lane') + + response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: billing_address)) + assert_success response + assert_equal 'SUCCESS', response.message + end - response = @gateway.purchase(@amount, @credit_card, @options.merge(options)) + def test_successful_purchase_with_no_address + credit_card = credit_card('4111111111111111', month: '12', year: '2035', verification_value: '345') + + options = @options.dup + options[:billing_address] = nil + options[:pm_type] = 'gb_mastercard_card' + + response = @gateway.purchase(@amount, credit_card, options) assert_success response assert_equal 'SUCCESS', response.message end @@ -59,11 +143,11 @@ def test_successful_purchase_using_ach response = @gateway.purchase(100000, @check, @ach_options) assert_success response assert_equal 'SUCCESS', response.message - assert_equal 'CLO', response.params['data']['status'] + assert_equal 'ACT', response.params['data']['status'] end def test_successful_purchase_with_options - options = @options.merge(metadata: @metadata, ewallet_id: 'ewallet_1a867a32b47158b30a8c17d42f12f3f1') + options = @options.merge(metadata: @metadata, ewallet_id: 'ewallet_897aca846f002686e14677541f78a0f4') response = @gateway.purchase(100000, @credit_card, options) assert_success response assert_equal 'SUCCESS', response.message @@ -136,7 +220,7 @@ def test_partial_refund def test_failed_refund response = @gateway.refund(@amount, '') assert_failure response - assert_equal 'The request tried to retrieve a payment, but the payment was not found. The request was rejected. Corrective action: Use a valid payment ID.', response.message + assert_equal 'The request attempted an operation that requires a payment ID, but the payment was not found. The request was rejected. Corrective action: Use the ID of a valid payment.', response.message end def test_failed_void_with_payment_method_error @@ -161,21 +245,9 @@ def test_successful_verify end def test_successful_verify_with_peso - options = { - pm_type: 'mx_visa_card', - currency: 'MXN' - } - response = @gateway.verify(@credit_card, options) - assert_success response - assert_equal 'SUCCESS', response.message - end - - def test_successful_verify_with_yen - options = { - pm_type: 'jp_visa_card', - currency: 'JPY' - } - response = @gateway.verify(@credit_card, options) + @options[:pm_type] = 'mx_visa_card' + @options[:currency] = 'MXN' + response = @gateway.verify(@credit_card, @options) assert_success response assert_equal 'SUCCESS', response.message end @@ -186,6 +258,44 @@ def test_failed_verify assert_equal 'Do Not Honor', response.message end + def test_successful_store_and_purchase + store = @gateway.store(@credit_card, @options) + assert_success store + assert store.params.dig('data', 'id') + assert store.params.dig('data', 'default_payment_method') + + # 3DS authorization is required on storing a payment method for future transactions + # This test verifies that the card id and customer id are sent with the purchase + purchase = @gateway.purchase(100, store.authorization, @options) + assert_match(/The request tried to use a card ID, but the cardholder has not completed the 3DS verification process./, purchase.message) + end + + def test_successful_store_and_unstore + store = @gateway.store(@credit_card, @options) + assert_success store + assert customer_id = store.params.dig('data', 'id') + assert store.params.dig('data', 'default_payment_method') + + unstore = @gateway.unstore(store.authorization) + assert_success unstore + assert_equal true, unstore.params.dig('data', 'deleted') + assert_equal customer_id, unstore.params.dig('data', 'id') + end + + def test_failed_store + store = @gateway.store(@declined_card, @options) + assert_failure store + end + + def test_failed_unstore + store = @gateway.store(@credit_card, @options) + assert_success store + assert store.params.dig('data', 'id') + + unstore = @gateway.unstore('') + assert_failure unstore + end + def test_invalid_login gateway = RapydGateway.new(secret_key: '', access_key: '') @@ -205,4 +315,101 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:secret_key], transcript) assert_scrubbed(@gateway.options[:access_key], transcript) end + + def test_transcript_scrubbing_with_ach + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @check, @ach_options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@check.account_number, transcript) + assert_scrubbed(@check.routing_number, transcript) + assert_scrubbed(@gateway.options[:secret_key], transcript) + assert_scrubbed(@gateway.options[:access_key], transcript) + end + + def test_successful_authorize_with_3ds_v1_options + options = @options.merge(three_d_secure: @three_d_secure) + options[:pm_type] = 'gb_visa_card' + options[:three_d_secure][:version] = '1.0.2' + + response = @gateway.authorize(105000, @credit_card, options) + assert_success response + assert_equal 'ACT', response.params['data']['status'] + assert_equal '3d_verification', response.params['data']['payment_method_data']['next_action'] + assert response.params['data']['redirect_url'] + end + + def test_successful_authorize_with_3ds_v2_options + options = @options.merge(three_d_secure: @three_d_secure) + options[:pm_type] = 'gb_visa_card' + + response = @gateway.authorize(105000, @credit_card, options) + assert_success response + assert_equal 'ACT', response.params['data']['status'] + assert_equal '3d_verification', response.params['data']['payment_method_data']['next_action'] + assert response.params['data']['redirect_url'] + end + + def test_successful_purchase_with_3ds_v2_gateway_specific + options = @options.merge(three_d_secure: { required: true }) + options[:pm_type] = 'gb_visa_card' + + response = @gateway.purchase(105000, @credit_card, options) + assert_success response + assert_equal 'ACT', response.params['data']['status'] + assert_equal '3d_verification', response.params['data']['payment_method_data']['next_action'] + assert response.params['data']['redirect_url'] + assert_match 'https://sandboxcheckout.rapyd.net/3ds-payment?token=payment_', response.params['data']['redirect_url'] + end + + def test_successful_purchase_without_3ds_v2_gateway_specific + options = @options.merge(three_d_secure: { required: false }) + options[:pm_type] = 'gb_visa_card' + response = @gateway.purchase(1000, @credit_card, options) + assert_success response + assert_equal 'CLO', response.params['data']['status'] + assert_equal 'not_applicable', response.params['data']['payment_method_data']['next_action'] + assert_equal '', response.params['data']['redirect_url'] + end + + def test_successful_authorize_with_execute_threed + ActiveSupport::JSON::Encoding.escape_html_entities_in_json = true + @options[:complete_payment_url] = 'http://www.google.com?param1=1¶m2=2' + options = @options.merge(pm_type: 'gb_visa_card', execute_threed: true) + response = @gateway.authorize(105000, @credit_card, options) + assert_success response + assert_equal 'ACT', response.params['data']['status'] + assert_equal '3d_verification', response.params['data']['payment_method_data']['next_action'] + assert response.params['data']['redirect_url'] + ensure + ActiveSupport::JSON::Encoding.escape_html_entities_in_json = false + end + + def test_successful_purchase_without_cvv + options = @options.merge({ pm_type: 'gb_visa_card', network_transaction_id: rand.to_s[2..11] }) + @credit_card.verification_value = nil + response = @gateway.purchase(100, @credit_card, options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_recurring_transaction_without_cvv + @credit_card.verification_value = nil + response = @gateway.purchase(15000, @credit_card, @stored_credential_options.merge(stored_credential: { network_transaction_id: rand.to_s[2..11], reason_type: 'recurring' })) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_empty_network_transaction_id + response = @gateway.purchase(15000, @credit_card, @stored_credential_options.merge(network_transaction_id: '', initiation_type: 'customer_present')) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_nil_network_transaction_id + response = @gateway.purchase(15000, @credit_card, @stored_credential_options.merge(network_transaction_id: nil, initiation_type: 'customer_present')) + assert_success response + assert_equal 'SUCCESS', response.message + end end diff --git a/test/remote/gateways/remote_reach_test.rb b/test/remote/gateways/remote_reach_test.rb new file mode 100644 index 00000000000..b72af06f159 --- /dev/null +++ b/test/remote/gateways/remote_reach_test.rb @@ -0,0 +1,326 @@ +require 'test_helper' + +class RemoteReachTest < Test::Unit::TestCase + def setup + @gateway = ReachGateway.new(fixtures(:reach)) + @amount = 100 + @credit_card = credit_card('4444333322221111', { + month: 3, + year: 2030, + verification_value: 737 + }) + @not_supported_cc = credit_card('4444333322221111', { + month: 3, + year: 2030, + verification_value: 737, + brand: 'alelo' + }) + @declined_card = credit_card('4000300011112220') + @options = { + email: 'johndoe@reach.com', + order_id: '123', + description: 'Store Purchase', + currency: 'USD', + billing_address: { + address1: '1670', + address2: '1670 NW 82ND AVE', + city: 'Miami', + state: 'FL', + zip: '32191', + country: 'US' + }, + device_fingerprint: fingerprint + } + @non_valid_authorization = SecureRandom.uuid + end + + def test_successful_authorize + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_failed_authorize + @options[:currency] = 'PPP' + @options.delete(:email) + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_failure response + assert_equal 'Invalid ConsumerCurrency', response.message + end + + def test_failed_authorize_with_not_supported_payment_method + response = @gateway.authorize(@amount, @not_supported_cc, @options) + + assert_failure response + assert_equal 'PaymentMethodUnsupported', response.error_code + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_equal 'NotATestCard', response.message + end + + def test_successful_purchase_with_fingerprint + @options[:device_fingerprint] = '54fd66c2-b5b5-4dbd-ab89-12a8b6177347' + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_successful_purchase_with_shipping_data + @options[:price] = '1.01' + @options[:taxes] = '2.01' + @options[:duty] = '1.01' + + @options[:consignee_name] = 'Jane Doe' + @options[:consignee_address] = '1670 NW 82ND STR' + @options[:consignee_city] = 'Houston' + @options[:consignee_country] = 'US' + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_failed_purchase_with_incomplete_shipping_data + @options[:price] = '1.01' + @options[:taxes] = '2.01' + + @options[:consignee_name] = 'Jane Doe' + @options[:consignee_address] = '1670 NW 82ND STR' + @options[:consignee_city] = 'Houston' + @options[:consignee_country] = 'US' + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_equal 'Invalid shipping values.', response.message + end + + def test_failed_purchase_with_shipping_data_and_no_consignee_info + @options[:price] = '1.01' + @options[:taxes] = '2.01' + @options[:duty] = '1.01' + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_equal 'Invalid JSON submitted', response.message + end + + def test_successful_purchase_with_items + @options[:items] = [ + { + Sku: SecureRandom.alphanumeric, + ConsumerPrice: '10', + Quantity: 1 + }, + { + Sku: SecureRandom.alphanumeric, + ConsumerPrice: '90', + Quantity: 1 + } + ] + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + # The Complete flag in the response returns false when capture is + # in progress + def test_successful_authorize_and_capture + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + response = @gateway.capture(@amount, response.authorization) + assert_success response + end + + def test_successful_purchase_with_store_credentials + @options[:stored_credential] = { initiator: 'cardholder', initial_transaction: true, reason_type: 'installment' } + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_successful_purchase_with_store_credentials_mit + @options[:stored_credential] = { initiator: 'merchant', initial_transaction: false, reason_type: 'recurring' } + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_successful_purchase_with_store_credentials_mit_and_network_transaction_id + @options[:stored_credential] = { initiator: 'cardholder', initial_transaction: true, reason_type: 'installment' } + purchase = @gateway.purchase(@amount, @credit_card, @options) + + @options[:stored_credential] = { initiator: 'merchant', initial_transaction: false, reason_type: 'unscheduled', network_transaction_id: purchase.network_transaction_id } + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + + assert response.params['response']['Authorized'] + assert response.params['response']['OrderId'] + end + + def test_failed_purchase_with_store_credentials_mit_and_network_transaction_id + @options[:stored_credential] = { initiator: 'merchant', initial_transaction: false, reason_type: 'unscheduled', network_transaction_id: 'uhh123' } + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + + assert_equal response.message, 'InvalidPreviousNetworkPaymentReference' + end + + def test_failed_purchase_payment_model_nil + @options[:stored_credential] = { initiator: 'merchant', initial_transaction: false, reason_type: 'installment', network_transaction_id: 'uhh123' } + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_equal 'Invalid PaymentModel', response.message + end + + def test_failed_capture + response = @gateway.capture(@amount, "#{@gateway.options[:merchant_id]}#123") + + assert_failure response + assert_equal 'Not Found', response.message + end + + def test_successful_refund_with_reference_id + response = @gateway.refund( + @amount, + '7d689cc1-4478-4e92-8cd9-f05528cde2f4', + { reference_id: 'REFUND_TAG' } + ) + + assert_success response + assert response.params['response']['RefundId'].present? + end + + def test_successful_refund_with_order_id + response = @gateway.refund( + @amount, + '7d689cc1-4478-4e92-8cd9-f05528cde2f4', + { order_id: 'REFUND_TAG' } + ) + + assert_success response + assert response.params['response']['RefundId'].present? + end + + def test_failed_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + response = @gateway.refund(@amount, purchase.authorization, { reference_id: 'REFUND_TAG' }) + + assert_failure response + assert_equal 'OrderStateInvalid', response.error_code + assert response.params['response']['OrderId'].present? + end + + def test_successful_void + authorize = @gateway.authorize(@amount, @credit_card, @options) + response = @gateway.void(authorize.authorization, @options) + + assert_success response + assert response.params['response']['OrderId'].present? + end + + def test_failed_void + response = @gateway.void(@non_valid_authorization, @options) + assert_failure response + + assert_equal 'Not Found', response.message + assert response.params.blank? + end + + def test_successful_partial_void + authorize = @gateway.authorize(@amount / 2, @credit_card, @options) + response = @gateway.void(authorize.authorization, @options) + + assert_success response + assert response.params['response']['OrderId'].present? + end + + def test_successful_void_higher_amount + authorize = @gateway.authorize(@amount * 2, @credit_card, @options) + response = @gateway.void(authorize.authorization, @options) + + assert_success response + assert response.params['response']['OrderId'].present? + end + + def test_successful_double_void_and_idempotent + authorize = @gateway.authorize(@amount, @credit_card, @options) + response = @gateway.void(authorize.authorization, @options) + + assert_success response + assert response.params['response']['OrderId'].present? + + second_void_response = @gateway.void(authorize.authorization, @options) + + assert_success second_void_response + assert second_void_response.params['response']['OrderId'].present? + + assert_equal response.params['response']['OrderId'], second_void_response.params['response']['OrderId'] + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + + assert_success response + assert response.params['response']['OrderId'].present? + end + + def test_failed_verify + response = @gateway.verify(@declined_card, @options) + + assert_failure response + assert response.params['response']['OrderId'].present? + assert_equal 'PaymentAuthorizationFailed', response.error_code + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + + transcript = @gateway.scrub(transcript) + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + assert_scrubbed(@gateway.options[:merchant_id], transcript) + assert_scrubbed(@gateway.options[:secret], transcript) + end + + def fingerprint + raw_response = @gateway.ssl_get @gateway.send(:url, "fingerprint?MerchantId=#{@gateway.options[:merchant_id]}") + + fingerprint = raw_response.match(/(gip_device_fingerprint=')([\w -]+)/)[2] + fingerprint + end +end diff --git a/test/remote/gateways/remote_realex_test.rb b/test/remote/gateways/remote_realex_test.rb index 39006c27d44..a45904d8e60 100644 --- a/test/remote/gateways/remote_realex_test.rb +++ b/test/remote/gateways/remote_realex_test.rb @@ -18,17 +18,21 @@ def setup @mastercard_referral_a = card_fixtures(:realex_mastercard_referral_a) @mastercard_coms_error = card_fixtures(:realex_mastercard_coms_error) - @apple_pay = network_tokenization_credit_card('4242424242424242', + @apple_pay = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :apple_pay) + source: :apple_pay + ) - @declined_apple_pay = network_tokenization_credit_card('4000120000001154', + @declined_apple_pay = network_tokenization_credit_card( + '4000120000001154', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :apple_pay) + source: :apple_pay + ) @amount = 10000 end @@ -38,13 +42,16 @@ def card_fixtures(name) def test_realex_purchase [@visa, @mastercard].each do |card| - response = @gateway.purchase(@amount, card, + response = @gateway.purchase( + @amount, + card, order_id: generate_unique_id, description: 'Test Realex Purchase', billing_address: { zip: '90210', country: 'US' - }) + } + ) assert_not_nil response assert_success response assert response.test? @@ -58,9 +65,12 @@ def test_realex_purchase_with_invalid_login login: 'invalid', password: 'invalid' ) - response = gateway.purchase(@amount, @visa, + response = gateway.purchase( + @amount, + @visa, order_id: generate_unique_id, - description: 'Invalid login test') + description: 'Invalid login test' + ) assert_not_nil response assert_failure response @@ -70,9 +80,12 @@ def test_realex_purchase_with_invalid_login end def test_realex_purchase_with_invalid_account - response = RealexGateway.new(fixtures(:realex_with_account).merge(account: 'invalid')).purchase(@amount, @visa, + response = RealexGateway.new(fixtures(:realex_with_account).merge(account: 'invalid')).purchase( + @amount, + @visa, order_id: generate_unique_id, - description: 'Test Realex purchase with invalid account') + description: 'Test Realex purchase with invalid account' + ) assert_not_nil response assert_failure response @@ -90,9 +103,12 @@ def test_realex_purchase_with_apple_pay def test_realex_purchase_declined [@visa_declined, @mastercard_declined].each do |card| - response = @gateway.purchase(@amount, card, + response = @gateway.purchase( + @amount, + card, order_id: generate_unique_id, - description: 'Test Realex purchase declined') + description: 'Test Realex purchase declined' + ) assert_not_nil response assert_failure response @@ -178,9 +194,12 @@ def test_subsequent_purchase_with_stored_credential def test_realex_purchase_referral_b [@visa_referral_b, @mastercard_referral_b].each do |card| - response = @gateway.purchase(@amount, card, + response = @gateway.purchase( + @amount, + card, order_id: generate_unique_id, - description: 'Test Realex Referral B') + description: 'Test Realex Referral B' + ) assert_not_nil response assert_failure response assert response.test? @@ -191,9 +210,12 @@ def test_realex_purchase_referral_b def test_realex_purchase_referral_a [@visa_referral_a, @mastercard_referral_a].each do |card| - response = @gateway.purchase(@amount, card, + response = @gateway.purchase( + @amount, + card, order_id: generate_unique_id, - description: 'Test Realex Rqeferral A') + description: 'Test Realex Rqeferral A' + ) assert_not_nil response assert_failure response assert_equal '103', response.params['result'] @@ -203,9 +225,12 @@ def test_realex_purchase_referral_a def test_realex_purchase_coms_error [@visa_coms_error, @mastercard_coms_error].each do |card| - response = @gateway.purchase(@amount, card, + response = @gateway.purchase( + @amount, + card, order_id: generate_unique_id, - description: 'Test Realex coms error') + description: 'Test Realex coms error' + ) assert_not_nil response assert_failure response @@ -217,9 +242,12 @@ def test_realex_purchase_coms_error def test_realex_expiry_month_error @visa.month = 13 - response = @gateway.purchase(@amount, @visa, + response = @gateway.purchase( + @amount, + @visa, order_id: generate_unique_id, - description: 'Test Realex expiry month error') + description: 'Test Realex expiry month error' + ) assert_not_nil response assert_failure response @@ -230,9 +258,12 @@ def test_realex_expiry_month_error def test_realex_expiry_year_error @visa.year = 2005 - response = @gateway.purchase(@amount, @visa, + response = @gateway.purchase( + @amount, + @visa, order_id: generate_unique_id, - description: 'Test Realex expiry year error') + description: 'Test Realex expiry year error' + ) assert_not_nil response assert_failure response @@ -244,9 +275,12 @@ def test_invalid_credit_card_name @visa.first_name = '' @visa.last_name = '' - response = @gateway.purchase(@amount, @visa, + response = @gateway.purchase( + @amount, + @visa, order_id: generate_unique_id, - description: 'test_chname_error') + description: 'test_chname_error' + ) assert_not_nil response assert_failure response @@ -257,32 +291,41 @@ def test_invalid_credit_card_name def test_cvn @visa_cvn = @visa.clone @visa_cvn.verification_value = '111' - response = @gateway.purchase(@amount, @visa_cvn, + response = @gateway.purchase( + @amount, + @visa_cvn, order_id: generate_unique_id, - description: 'test_cvn') + description: 'test_cvn' + ) assert_not_nil response assert_success response assert response.authorization.length > 0 end def test_customer_number - response = @gateway.purchase(@amount, @visa, + response = @gateway.purchase( + @amount, + @visa, order_id: generate_unique_id, description: 'test_cust_num', - customer: 'my customer id') + customer: 'my customer id' + ) assert_not_nil response assert_success response assert response.authorization.length > 0 end def test_realex_authorize - response = @gateway.authorize(@amount, @visa, + response = @gateway.authorize( + @amount, + @visa, order_id: generate_unique_id, description: 'Test Realex Purchase', billing_address: { zip: '90210', country: 'US' - }) + } + ) assert_not_nil response assert_success response @@ -294,13 +337,16 @@ def test_realex_authorize def test_realex_authorize_then_capture order_id = generate_unique_id - auth_response = @gateway.authorize(@amount, @visa, + auth_response = @gateway.authorize( + @amount, + @visa, order_id: order_id, description: 'Test Realex Purchase', billing_address: { zip: '90210', country: 'US' - }) + } + ) assert auth_response.test? capture_response = @gateway.capture(nil, auth_response.authorization) @@ -315,13 +361,16 @@ def test_realex_authorize_then_capture def test_realex_authorize_then_capture_with_extra_amount order_id = generate_unique_id - auth_response = @gateway.authorize(@amount * 115, @visa, + auth_response = @gateway.authorize( + @amount * 115, + @visa, order_id: order_id, description: 'Test Realex Purchase', billing_address: { zip: '90210', country: 'US' - }) + } + ) assert auth_response.test? capture_response = @gateway.capture(@amount, auth_response.authorization) @@ -336,13 +385,16 @@ def test_realex_authorize_then_capture_with_extra_amount def test_realex_purchase_then_void order_id = generate_unique_id - purchase_response = @gateway.purchase(@amount, @visa, + purchase_response = @gateway.purchase( + @amount, + @visa, order_id: order_id, description: 'Test Realex Purchase', billing_address: { zip: '90210', country: 'US' - }) + } + ) assert purchase_response.test? void_response = @gateway.void(purchase_response.authorization) @@ -358,13 +410,16 @@ def test_realex_purchase_then_refund gateway_with_refund_password = RealexGateway.new(fixtures(:realex).merge(rebate_secret: 'rebate')) - purchase_response = gateway_with_refund_password.purchase(@amount, @visa, + purchase_response = gateway_with_refund_password.purchase( + @amount, + @visa, order_id: order_id, description: 'Test Realex Purchase', billing_address: { zip: '90210', country: 'US' - }) + } + ) assert purchase_response.test? rebate_response = gateway_with_refund_password.refund(@amount, purchase_response.authorization) @@ -376,9 +431,11 @@ def test_realex_purchase_then_refund end def test_realex_verify - response = @gateway.verify(@visa, + response = @gateway.verify( + @visa, order_id: generate_unique_id, - description: 'Test Realex verify') + description: 'Test Realex verify' + ) assert_not_nil response assert_success response @@ -388,9 +445,11 @@ def test_realex_verify end def test_realex_verify_declined - response = @gateway.verify(@visa_declined, + response = @gateway.verify( + @visa_declined, order_id: generate_unique_id, - description: 'Test Realex verify declined') + description: 'Test Realex verify declined' + ) assert_not_nil response assert_failure response @@ -402,13 +461,16 @@ def test_realex_verify_declined def test_successful_credit gateway_with_refund_password = RealexGateway.new(fixtures(:realex).merge(refund_secret: 'refund')) - credit_response = gateway_with_refund_password.credit(@amount, @visa, + credit_response = gateway_with_refund_password.credit( + @amount, + @visa, order_id: generate_unique_id, description: 'Test Realex Credit', billing_address: { zip: '90210', country: 'US' - }) + } + ) assert_not_nil credit_response assert_success credit_response @@ -417,13 +479,16 @@ def test_successful_credit end def test_failed_credit - credit_response = @gateway.credit(@amount, @visa, + credit_response = @gateway.credit( + @amount, + @visa, order_id: generate_unique_id, description: 'Test Realex Credit', billing_address: { zip: '90210', country: 'US' - }) + } + ) assert_not_nil credit_response assert_failure credit_response @@ -433,13 +498,16 @@ def test_failed_credit def test_maps_avs_and_cvv_response_codes [@visa, @mastercard].each do |card| - response = @gateway.purchase(@amount, card, + response = @gateway.purchase( + @amount, + card, order_id: generate_unique_id, description: 'Test Realex Purchase', billing_address: { zip: '90210', country: 'US' - }) + } + ) assert_not_nil response assert_success response assert_equal 'M', response.avs_result['code'] @@ -449,13 +517,16 @@ def test_maps_avs_and_cvv_response_codes def test_transcript_scrubbing transcript = capture_transcript(@gateway) do - @gateway.purchase(@amount, @visa_declined, + @gateway.purchase( + @amount, + @visa_declined, order_id: generate_unique_id, description: 'Test Realex Purchase', billing_address: { zip: '90210', country: 'US' - }) + } + ) end clean_transcript = @gateway.scrub(transcript) diff --git a/test/remote/gateways/remote_redsys_sha256_test.rb b/test/remote/gateways/remote_redsys_sha256_test.rb index da736112b3b..334e4cc21ca 100644 --- a/test/remote/gateways/remote_redsys_sha256_test.rb +++ b/test/remote/gateways/remote_redsys_sha256_test.rb @@ -212,6 +212,27 @@ def test_successful_3ds2_purchase_with_mit_exemption assert_match 'MIT', response.params['ds_excep_sca'] end + def test_successful_ntid_generation_with_verify + # This test validates a special use case for generating a network transaction id + # for payment methods that previously relied on the 'magic' fifteen-nines value. + # However, the transaction id will only be returned from a Redsys production + # environment, so this test simply validates that the zero-value request succeeds. + + alternate_gateway = RedsysGateway.new(fixtures(:redsys_sha256_alternate)) + options = @options.merge( + sca_exemption_behavior_override: 'endpoint_and_ntid', + sca_exemption: 'MIT', + terminal: 2, + stored_credential: { + initial_transaction: false, + reason_type: 'recurring', + network_transaction_id: '999999999999999' + } + ) + response = alternate_gateway.verify(@credit_card, options) + assert_success response + end + def test_failed_3ds2_purchase_with_mit_exemption_but_missing_direct_payment_enabled options = @options.merge(execute_threed: true, terminal: 12) response = @gateway.purchase(@amount, @threeds2_credit_card, options.merge(sca_exemption: 'MIT')) @@ -279,7 +300,7 @@ def test_successful_purchase_using_vault_id def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'SIS0093 ERROR', response.message + assert_equal 'Refusal with no specific reason', response.message end def test_purchase_and_refund @@ -326,7 +347,7 @@ def test_successful_authorise_using_vault_id def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_equal 'SIS0093 ERROR', response.message + assert_equal 'Refusal with no specific reason', response.message end def test_successful_void @@ -363,7 +384,7 @@ def test_successful_verify def test_unsuccessful_verify assert response = @gateway.verify(@declined_card, @options) assert_failure response - assert_equal 'SIS0093 ERROR', response.message + assert_equal 'Refusal with no specific reason', response.message end def test_transcript_scrubbing diff --git a/test/remote/gateways/remote_redsys_test.rb b/test/remote/gateways/remote_redsys_test.rb index 688045c1965..b8c790d30f9 100644 --- a/test/remote/gateways/remote_redsys_test.rb +++ b/test/remote/gateways/remote_redsys_test.rb @@ -111,7 +111,7 @@ def test_successful_purchase_using_vault_id def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'SIS0093 ERROR', response.message + assert_equal 'Refusal with no specific reason', response.message end def test_purchase_and_refund @@ -158,7 +158,7 @@ def test_successful_authorise_using_vault_id def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_equal 'SIS0093 ERROR', response.message + assert_equal 'Refusal with no specific reason', response.message end def test_successful_void @@ -195,7 +195,7 @@ def test_successful_verify def test_unsuccessful_verify assert response = @gateway.verify(@declined_card, @options) assert_failure response - assert_equal 'SIS0093 ERROR', response.message + assert_equal 'Refusal with no specific reason', response.message end def test_transcript_scrubbing @@ -250,6 +250,17 @@ def test_whitespace_string_cvv_transcript_scrubbing assert_equal clean_transcript.include?('[BLANK]'), true end + def test_encrypt_handles_url_safe_character_in_secret_key_without_error + gateway = RedsysGateway.new({ + login: '091952713', + secret_key: 'yG78qf-PkHyRzRiZGSTCJdO2TvjWgFa8', + terminal: '1', + signature_algorithm: 'sha256' + }) + response = gateway.purchase(@amount, @credit_card, @options) + assert response + end + private def generate_order_id diff --git a/test/remote/gateways/remote_safe_charge_test.rb b/test/remote/gateways/remote_safe_charge_test.rb index 158e1464518..ee1a0295e09 100644 --- a/test/remote/gateways/remote_safe_charge_test.rb +++ b/test/remote/gateways/remote_safe_charge_test.rb @@ -256,6 +256,18 @@ def test_failed_refund assert_equal 'Transaction must contain a Card/Token/Account', response.message end + def test_successful_unreferenced_refund + option = { + unreferenced_refund: true + } + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization, option) + assert_success refund + assert_equal 'Success', refund.message + end + def test_successful_credit response = @gateway.credit(@amount, credit_card('4444436501403986'), @options) assert_success response @@ -282,6 +294,12 @@ def test_successful_credit_with_extra_options assert_equal 'Success', response.message end + def test_successful_credit_with_customer_details + response = @gateway.credit(@amount, credit_card('4444436501403986'), @options.merge(email: 'test@example.com')) + assert_success response + assert_equal 'Success', response.message + end + def test_failed_credit response = @gateway.credit(@amount, @declined_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_sage_pay_test.rb b/test/remote/gateways/remote_sage_pay_test.rb index fdc8543d2d1..487332e1097 100644 --- a/test/remote/gateways/remote_sage_pay_test.rb +++ b/test/remote/gateways/remote_sage_pay_test.rb @@ -25,7 +25,6 @@ def setup number: '5641820000000005', month: 12, year: next_year, - issue_number: '01', start_month: 12, start_year: next_year - 2, verification_value: 123, @@ -116,6 +115,19 @@ def test_unsuccessful_purchase assert response.test? end + def test_successful_purchase_via_reference + assert initial_response = @gateway.purchase(@amount, @mastercard, @options) + assert_success initial_response + + options = @options.merge(order_id: generate_unique_id) + assert first_reference_response = @gateway.purchase(@amount, initial_response.authorization, options) + assert_success first_reference_response + + options = @options.merge(order_id: generate_unique_id) + assert second_reference_response = @gateway.purchase(@amount, first_reference_response.authorization, options) + assert_success second_reference_response + end + def test_successful_authorization_and_capture assert auth = @gateway.authorize(@amount, @mastercard, @options) assert_success auth @@ -130,10 +142,7 @@ def test_successful_authorization_and_capture_and_refund assert capture = @gateway.capture(@amount, auth.authorization) assert_success capture - - assert refund = @gateway.refund(@amount, capture.authorization, - description: 'Crediting trx', - order_id: generate_unique_id) + assert refund = @gateway.refund(@amount, capture.authorization, description: 'Crediting trx', order_id: generate_unique_id) assert_success refund end @@ -156,11 +165,7 @@ def test_successful_purchase_and_void def test_successful_purchase_and_refund assert purchase = @gateway.purchase(@amount, @mastercard, @options) assert_success purchase - - assert refund = @gateway.refund(@amount, purchase.authorization, - description: 'Crediting trx', - order_id: generate_unique_id) - + assert refund = @gateway.refund(@amount, purchase.authorization, description: 'Crediting trx', order_id: generate_unique_id) assert_success refund end @@ -234,7 +239,7 @@ def test_successful_purchase_with_apply_avscv2_field @options[:apply_avscv2] = 1 response = @gateway.purchase(@amount, @visa, @options) assert_success response - assert_equal 'Y', response.cvv_result['code'] + assert_equal 'M', response.cvv_result['code'] end def test_successful_purchase_with_pay_pal_callback_url diff --git a/test/remote/gateways/remote_sage_test.rb b/test/remote/gateways/remote_sage_test.rb index 8ec91e95810..1bbca7cbad1 100644 --- a/test/remote/gateways/remote_sage_test.rb +++ b/test/remote/gateways/remote_sage_test.rb @@ -164,8 +164,7 @@ def test_partial_refund def test_store_visa assert response = @gateway.store(@visa, @options) assert_success response - assert response.authorization, - 'Store card authorization should not be nil' + assert response.authorization, 'Store card authorization should not be nil' assert_not_nil response.message end @@ -176,15 +175,13 @@ def test_failed_store end def test_unstore_visa - assert auth = @gateway.store(@visa, @options).authorization, - 'Unstore card authorization should not be nil' + assert auth = @gateway.store(@visa, @options).authorization, 'Unstore card authorization should not be nil' assert response = @gateway.unstore(auth, @options) assert_success response end def test_failed_unstore_visa - assert auth = @gateway.store(@visa, @options).authorization, - 'Unstore card authorization should not be nil' + assert auth = @gateway.store(@visa, @options).authorization, 'Unstore card authorization should not be nil' assert response = @gateway.unstore(auth, @options) assert_success response end diff --git a/test/remote/gateways/remote_secure_pay_test.rb b/test/remote/gateways/remote_secure_pay_test.rb index dec1ff43d41..77d41451b3b 100644 --- a/test/remote/gateways/remote_secure_pay_test.rb +++ b/test/remote/gateways/remote_secure_pay_test.rb @@ -4,9 +4,7 @@ class RemoteSecurePayTest < Test::Unit::TestCase def setup @gateway = SecurePayGateway.new(fixtures(:secure_pay)) - @credit_card = credit_card('4111111111111111', - month: '7', - year: '2014') + @credit_card = credit_card('4111111111111111', month: '7', year: '2014') @options = { order_id: generate_unique_id, diff --git a/test/remote/gateways/remote_securion_pay_test.rb b/test/remote/gateways/remote_securion_pay_test.rb index b2ffead799f..6e83fa91d76 100644 --- a/test/remote/gateways/remote_securion_pay_test.rb +++ b/test/remote/gateways/remote_securion_pay_test.rb @@ -20,15 +20,28 @@ def setup } end + def test_successful_store_and_purchase + response = @gateway.store(@credit_card, @options) + assert_success response + assert_equal 'customer', response.params['objectType'] + assert_match %r(^card_\w+$), response.params['cards'][0]['id'] + assert_equal 'card', response.params['cards'][0]['objectType'] + + @options[:customer_id] = response.params['cards'][0]['customerId'] + + response = @gateway.purchase(@amount, response.authorization, @options) + assert_success response + assert_equal 'Transaction approved', response.message + end + def test_successful_store response = @gateway.store(@credit_card, @options) assert_success response - assert_match %r(^cust_\w+$), response.authorization assert_equal 'customer', response.params['objectType'] assert_match %r(^card_\w+$), response.params['cards'][0]['id'] assert_equal 'card', response.params['cards'][0]['objectType'] - @options[:customer_id] = response.authorization + @options[:customer_id] = response.params['cards'][0]['customerId'] response = @gateway.store(@new_credit_card, @options) assert_success response assert_match %r(^card_\w+$), response.params['card']['id'] @@ -43,11 +56,6 @@ def test_successful_store assert_equal '4242', response.params['cards'][1]['last4'] end - # def test_dump_transcript - # skip("Transcript scrubbing for this gateway has been tested.") - # dump_transcript_and_fail(@gateway, @amount, @credit_card, @options) - # end - def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card, @options) @@ -81,7 +89,7 @@ def test_successful_purchase_with_three_ds_data def test_unsuccessful_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_match %r{The card was declined for other reason.}, response.message + assert_match %r{The card was declined}, response.message assert_match Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code end @@ -104,7 +112,7 @@ def test_failed_authorize def test_failed_capture response = @gateway.capture(@amount, 'invalid_authorization_token') assert_failure response - assert_match %r{Requested Charge does not exist}, response.message + assert_match %r{Charge 'invalid_authorization_token' does not exist}, response.message end def test_successful_full_refund @@ -116,7 +124,7 @@ def test_successful_full_refund assert_success refund assert refund.params['refunded'] - assert_equal 0, refund.params['amount'] + assert_equal 2000, refund.params['refunds'].first['amount'] assert_equal 1, refund.params['refunds'].size assert_equal @amount, refund.params['refunds'].map { |r| r['amount'] }.sum @@ -130,6 +138,7 @@ def test_successful_partially_refund first_refund = @gateway.refund(@refund_amount, purchase.authorization) assert_success first_refund + assert_equal @refund_amount, first_refund.params['refunds'].first['amount'] second_refund = @gateway.refund(@refund_amount, purchase.authorization) assert_success second_refund @@ -143,7 +152,7 @@ def test_successful_partially_refund def test_unsuccessful_authorize_refund response = @gateway.refund(@amount, 'invalid_authorization_token') assert_failure response - assert_match %r{Requested Charge does not exist}, response.message + assert_match %r{Charge 'invalid_authorization_token' does not exist}, response.message end def test_unsuccessful_refund @@ -173,7 +182,7 @@ def test_successful_void def test_failed_void response = @gateway.void('invalid_authorization_token', @options) assert_failure response - assert_match %r{Requested Charge does not exist}, response.message + assert_match %r{Charge 'invalid_authorization_token' does not exist}, response.message end def test_successful_verify @@ -185,7 +194,7 @@ def test_successful_verify def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response - assert_match %r{The card was declined for other reason.}, response.message + assert_match %r{The card was declined}, response.message assert_match Gateway::STANDARD_ERROR_CODE[:card_declined], response.primary_response.error_code end diff --git a/test/remote/gateways/remote_shift4_test.rb b/test/remote/gateways/remote_shift4_test.rb new file mode 100644 index 00000000000..4403868aa71 --- /dev/null +++ b/test/remote/gateways/remote_shift4_test.rb @@ -0,0 +1,270 @@ +require 'test_helper' + +class RemoteShift4Test < Test::Unit::TestCase + def setup + @gateway = Shift4Gateway.new(fixtures(:shift4)) + access_token = @gateway.setup_access_token + + @gateway = Shift4Gateway.new(fixtures(:shift4).merge(access_token: access_token)) + + @amount = 500 + @credit_card = credit_card('4000100011112224', verification_value: '333', first_name: 'John', last_name: 'Smith') + @declined_card = credit_card('400030001111220', first_name: 'John', last_name: 'Doe') + @unsupported_card = credit_card('4000100011112224', verification_value: '333', first_name: 'John', last_name: '成龙') + @options = {} + @extra_options = { + clerk_id: '1576', + notes: 'test notes', + tax: '2', + customer_reference: 'D019D09309F2', + destination_postal_code: '94719', + product_descriptors: %w(Hamburger Fries Soda Cookie), + order_id: '123456' + } + @customer_address = { + address1: '65 Easy St', + zip: '65144' + } + end + + def test_successful_authorize + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert_equal response.message, 'Transaction successful' + end + + def test_successful_authorize_with_extra_options + response = @gateway.authorize(@amount, @credit_card, @options.merge(@extra_options)) + assert_success response + assert_equal response.message, 'Transaction successful' + end + + def test_successful_authorize_with_store + response = @gateway.store(@credit_card, @options) + assert_success response + assert_not_empty response.authorization + assert_not_include response.authorization, '|' + + response = @gateway.authorize(@amount, response.authorization, @options) + assert_success response + assert_include 'Transaction successful', response.message + end + + def test_successful_capture + authorize_res = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize_res + response = @gateway.capture(@amount, authorize_res.authorization, @options) + + assert_success response + assert_equal response.message, 'Transaction successful' + assert response_result(response)['transaction']['invoice'].present? + assert_equal response_result(response)['transaction']['invoice'], response_result(authorize_res)['transaction']['invoice'] + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_include 'Transaction successful', response.message + end + + def test_successful_purchase_with_customer_details + response = @gateway.purchase(@amount, @credit_card, @options.merge({ billing_address: @customer_address })) + assert_success response + assert_include 'Transaction successful', response.message + end + + def test_successful_purchase_with_extra_options + response = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options)) + assert_success response + end + + def test_successful_purchase_passes_vendor_reference + response = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options)) + assert_success response + assert_equal response_result(response)['transaction']['vendorReference'], @extra_options[:order_id] + end + + def test_successful_purchase_with_stored_credential_framework + stored_credential_options = { + initial_transaction: true, + reason_type: 'recurring' + } + @options[:merchant_time_zone] = 'EST' + first_response = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options.merge({ stored_credential: stored_credential_options }))) + assert_success first_response + + ntxid = first_response.params['result'].first['transaction']['cardOnFile']['transactionId'] + stored_credential_options = { + initial_transaction: false, + reason_type: 'recurring', + network_transaction_id: ntxid + } + response = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options.merge({ stored_credential: stored_credential_options }))) + assert_success response + end + + def test_successful_purchase_with_card_on_file_fields + card_on_file_fields = { + usage_indicator: '01', + indicator: '01', + scheduled_indicator: '01' + } + first_response = @gateway.purchase(@amount, @credit_card, @options.merge(card_on_file_fields)) + assert_success first_response + + ntxid = first_response.params['result'].first['transaction']['cardOnFile']['transactionId'] + card_on_file_fields = { + usage_indicator: '02', + indicator: '01', + scheduled_indicator: '02', + transaction_id: ntxid + } + response = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options.merge(card_on_file_fields))) + assert_success response + end + + def test_successful_purchase_with_store + response = @gateway.store(@credit_card, @options) + assert_success response + assert_not_empty response.authorization + + response = @gateway.purchase(@amount, response.authorization, @options) + assert_success response + assert_include 'Transaction successful', response.message + end + + def test_successful_purchase_with_store_having_customer_details + response = @gateway.store(@credit_card, @options.merge({ billing_address: @customer_address })) + assert_success response + assert_not_empty response.authorization + + response = @gateway.purchase(@amount, response.authorization, @options) + assert_success response + assert_include 'Transaction successful', response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + end + + def test_successful_verify_with_card_on_file_fields + card_on_file_fields = { + usage_indicator: '01', + indicator: '01', + scheduled_indicator: '01' + } + first_response = @gateway.purchase(@amount, @credit_card, @options.merge(card_on_file_fields)) + assert_success first_response + ntxid = first_response.params['result'].first['transaction']['cardOnFile']['transactionId'] + + card_on_file_fields = { + usage_indicator: '02', + indicator: '01', + scheduled_indicator: '02', + transaction_id: ntxid + } + response = @gateway.verify(@credit_card, @options.merge(card_on_file_fields)) + assert_success response + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.authorize(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed("0#{@credit_card.month}#{@credit_card.year.to_s[2..4]}", transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_include response.message, 'Card for Merchant Id 0008628968 not found' + end + + def test_failure_on_referral_transactions + response = @gateway.purchase(67800, @credit_card, @options) + assert_failure response + assert_include 'Transaction declined', response.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_include response.message, 'Card for Merchant Id 0008628968 not found' + end + + def test_failed_authorize_with_error_message + response = @gateway.authorize(@amount, @unsupported_card, @options) + assert_failure response + assert_equal response.message, 'Format \'UTF8: An unexpected continuatio\' invalid or incompatible with argument' + end + + def test_failed_capture + response = @gateway.capture(@amount, '', @options) + assert_failure response + assert_include response.message, 'Card for Merchant Id 0008628968 not found' + end + + def test_failed_refund + response = @gateway.refund(@amount, 'YC', @options) + assert_failure response + assert_include response.message, 'record not posted' + end + + def test_successful_credit + response = @gateway.credit(@amount, @credit_card, @options) + assert_success response + assert_equal response.message, 'Transaction successful' + end + + def test_failed_credit + response = @gateway.credit(@amount, @declined_card, @options) + assert_failure response + assert_include response.message, 'Card type not recognized' + end + + def test_successful_refund + res = @gateway.purchase(@amount, @credit_card, @options) + assert_success res + response = @gateway.refund(@amount, res.authorization, @options) + assert_success response + end + + def test_successful_refund_with_expiration_date + res = @gateway.purchase(@amount, @credit_card, @options) + assert_success res + response = @gateway.refund(@amount, res.authorization, @options.merge({ expiration_date: '1235' })) + assert_success response + end + + def test_successful_void + authorize_res = @gateway.authorize(@amount, @credit_card, @options) + assert response = @gateway.void(authorize_res.authorization, @options) + + assert_success response + assert_equal @options[:invoice], response_result(response)['transaction']['invoice'] + end + + def test_failed_void + response = @gateway.void('', @options) + assert_failure response + assert_include response.message, 'Invoice Not Found' + end + + def test_failed_access_token + gateway = Shift4Gateway.new({ client_guid: 'YOUR_CLIENT_ID', auth_token: 'YOUR_AUTH_TOKEN' }) + assert_raises(ActiveMerchant::OAuthResponseError) do + gateway.setup_access_token + end + end + + private + + def response_result(response) + response.params['result'][0] + end +end diff --git a/test/remote/gateways/remote_shift4_v2_test.rb b/test/remote/gateways/remote_shift4_v2_test.rb new file mode 100644 index 00000000000..7d501b34dd3 --- /dev/null +++ b/test/remote/gateways/remote_shift4_v2_test.rb @@ -0,0 +1,80 @@ +require 'test_helper' +require_relative 'remote_securion_pay_test' + +class RemoteShift4V2Test < RemoteSecurionPayTest + def setup + super + @gateway = Shift4V2Gateway.new(fixtures(:shift4_v2)) + end + + def test_successful_purchase_third_party_token + auth = @gateway.store(@credit_card, @options) + token = auth.params['defaultCardId'] + customer_id = auth.params['id'] + response = @gateway.purchase(@amount, token, @options.merge!(customer_id: customer_id)) + assert_success response + assert_equal 'Transaction approved', response.message + assert_equal 'foo@example.com', response.params['metadata']['email'] + assert_match CHARGE_ID_REGEX, response.authorization + end + + def test_unsuccessful_purchase_third_party_token + auth = @gateway.store(@credit_card, @options) + customer_id = auth.params['id'] + response = @gateway.purchase(@amount, @invalid_token, @options.merge!(customer_id: customer_id)) + assert_failure response + assert_equal "Token 'tok_invalid' does not exist", response.message + end + + def test_successful_stored_credentials_first_recurring + stored_credentials = { + initiator: 'cardholder', + reason_type: 'recurring' + } + @options.merge!(stored_credential: stored_credentials) + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Transaction approved', response.message + assert_equal 'first_recurring', response.params['type'] + assert_match CHARGE_ID_REGEX, response.authorization + end + + def test_successful_stored_credentials_subsequent_recurring + stored_credentials = { + initiator: 'merchant', + reason_type: 'recurring' + } + @options.merge!(stored_credential: stored_credentials) + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Transaction approved', response.message + assert_equal 'subsequent_recurring', response.params['type'] + assert_match CHARGE_ID_REGEX, response.authorization + end + + def test_successful_stored_credentials_customer_initiated + stored_credentials = { + initiator: 'cardholder', + reason_type: 'unscheduled' + } + @options.merge!(stored_credential: stored_credentials) + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Transaction approved', response.message + assert_equal 'customer_initiated', response.params['type'] + assert_match CHARGE_ID_REGEX, response.authorization + end + + def test_successful_stored_credentials_merchant_initiated + stored_credentials = { + initiator: 'merchant', + reason_type: 'installment' + } + @options.merge!(stored_credential: stored_credentials) + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Transaction approved', response.message + assert_equal 'merchant_initiated', response.params['type'] + assert_match CHARGE_ID_REGEX, response.authorization + end +end diff --git a/test/remote/gateways/remote_simetrik_test.rb b/test/remote/gateways/remote_simetrik_test.rb index e0c50ce6f71..b1e2eb24daf 100644 --- a/test/remote/gateways/remote_simetrik_test.rb +++ b/test/remote/gateways/remote_simetrik_test.rb @@ -49,10 +49,10 @@ def setup id: '123', email: 's@example.com' }, + order_id: rand(100000000000..999999999999).to_s, + description: 'apopsicle', order: { - id: rand(100000000000..999999999999).to_s, datetime_local_transaction: Time.new.strftime('%Y-%m-%dT%H:%M:%S.%L%:z'), - description: 'apopsicle', installments: 1 }, vat: 19, diff --git a/test/remote/gateways/remote_skipjack_test.rb b/test/remote/gateways/remote_skipjack_test.rb index 096aaee9602..1c53076c8ff 100644 --- a/test/remote/gateways/remote_skipjack_test.rb +++ b/test/remote/gateways/remote_skipjack_test.rb @@ -6,8 +6,7 @@ def setup @gateway = SkipJackGateway.new(fixtures(:skip_jack)) - @credit_card = credit_card('4445999922225', - verification_value: '999') + @credit_card = credit_card('4445999922225', verification_value: '999') @amount = 100 diff --git a/test/remote/gateways/remote_stripe_android_pay_test.rb b/test/remote/gateways/remote_stripe_android_pay_test.rb index 6daee95f62b..7adda284d40 100644 --- a/test/remote/gateways/remote_stripe_android_pay_test.rb +++ b/test/remote/gateways/remote_stripe_android_pay_test.rb @@ -15,11 +15,13 @@ def setup end def test_successful_purchase_with_android_pay_raw_cryptogram - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :android_pay) + source: :android_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'charge', response.params['object'] @@ -30,11 +32,13 @@ def test_successful_purchase_with_android_pay_raw_cryptogram end def test_successful_auth_with_android_pay_raw_cryptogram - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :android_pay) + source: :android_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'charge', response.params['object'] diff --git a/test/remote/gateways/remote_stripe_apple_pay_test.rb b/test/remote/gateways/remote_stripe_apple_pay_test.rb index f6d844feaba..b1a2f8c92aa 100644 --- a/test/remote/gateways/remote_stripe_apple_pay_test.rb +++ b/test/remote/gateways/remote_stripe_apple_pay_test.rb @@ -101,11 +101,13 @@ def test_purchase_with_unsuccessful_apple_pay_token_exchange end def test_successful_purchase_with_apple_pay_raw_cryptogram_with_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'charge', response.params['object'] @@ -116,10 +118,12 @@ def test_successful_purchase_with_apple_pay_raw_cryptogram_with_eci end def test_successful_purchase_with_apple_pay_raw_cryptogram_without_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'charge', response.params['object'] @@ -130,11 +134,13 @@ def test_successful_purchase_with_apple_pay_raw_cryptogram_without_eci end def test_successful_auth_with_apple_pay_raw_cryptogram_with_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'charge', response.params['object'] @@ -145,10 +151,12 @@ def test_successful_auth_with_apple_pay_raw_cryptogram_with_eci end def test_successful_auth_with_apple_pay_raw_cryptogram_without_eci - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'charge', response.params['object'] diff --git a/test/remote/gateways/remote_stripe_payment_intents_test.rb b/test/remote/gateways/remote_stripe_payment_intents_test.rb index 615562e2c11..14811db6d95 100644 --- a/test/remote/gateways/remote_stripe_payment_intents_test.rb +++ b/test/remote/gateways/remote_stripe_payment_intents_test.rb @@ -3,7 +3,7 @@ class RemoteStripeIntentsTest < Test::Unit::TestCase def setup @gateway = StripePaymentIntentsGateway.new(fixtures(:stripe)) - @customer = fixtures(:stripe_verified_bank_account)[:customer_id] + @customer = @gateway.create_test_customer @amount = 2000 @three_ds_payment_method = 'pm_card_threeDSecure2Required' @visa_payment_method = 'pm_card_visa' @@ -11,30 +11,42 @@ def setup @three_ds_moto_enabled = 'pm_card_authenticationRequiredOnSetup' @three_ds_authentication_required = 'pm_card_authenticationRequired' @three_ds_authentication_required_setup_for_off_session = 'pm_card_authenticationRequiredSetupForOffSession' - @three_ds_off_session_credit_card = credit_card('4000002500003155', + @three_ds_off_session_credit_card = credit_card( + '4000002500003155', verification_value: '737', month: 10, - year: 2028) - @three_ds_1_credit_card = credit_card('4000000000003063', + year: 2028 + ) + @three_ds_1_credit_card = credit_card( + '4000000000003063', verification_value: '737', month: 10, - year: 2028) - @three_ds_credit_card = credit_card('4000000000003220', + year: 2028 + ) + @three_ds_credit_card = credit_card( + '4000000000003220', verification_value: '737', month: 10, - year: 2028) - @three_ds_not_required_card = credit_card('4000000000003055', + year: 2028 + ) + @three_ds_not_required_card = credit_card( + '4000000000003055', verification_value: '737', month: 10, - year: 2028) - @three_ds_external_data_card = credit_card('4000002760003184', + year: 2028 + ) + @three_ds_external_data_card = credit_card( + '4000002760003184', verification_value: '737', month: 10, - year: 2031) - @visa_card = credit_card('4242424242424242', + year: 2031 + ) + @visa_card = credit_card( + '4242424242424242', verification_value: '737', month: 10, - year: 2028) + year: 2028 + ) @google_pay = network_tokenization_credit_card( '4242424242424242', @@ -60,6 +72,17 @@ def setup last_name: 'Longsen' ) + @network_token_credit_card = network_tokenization_credit_card( + '4000056655665556', + payment_cryptogram: 'AAEBAwQjSQAAXXXXXXXJYe0BbQA=', + source: :network_token, + brand: 'visa', + month: '09', + year: '2030', + first_name: 'Longbob', + last_name: 'Longsen' + ) + @destination_account = fixtures(:stripe_destination)[:stripe_user_id] end @@ -83,9 +106,10 @@ def test_successful_purchase customer: @customer } assert purchase = @gateway.purchase(@amount, @visa_payment_method, options) - assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + assert purchase.params.dig('charges', 'data')[0]['balance_transaction'] end def test_successful_purchase_with_shipping_address @@ -100,12 +124,48 @@ def test_successful_purchase_with_shipping_address address1: 'block C', address2: 'street 48', zip: '22400', - state: 'California' + state: 'California', + email: 'test@email.com' } } + assert response = @gateway.purchase(@amount, @visa_payment_method, options) assert_success response assert_equal 'succeeded', response.params['status'] + assert_nil response.params['shipping']['email'] + end + + def test_successful_purchase_with_level3_data + options = { + currency: 'USD', + customer: @customer, + merchant_reference: 123, + customer_reference: 456, + shipping_address_zip: 71601, + shipping_from_zip: 71601, + shipping_amount: 10, + line_items: [ + { + 'product_code' => 1234, + 'product_description' => 'An item', + 'unit_cost' => 15, + 'quantity' => 2, + 'tax_amount' => 0 + }, + { + 'product_code' => 999, + 'product_description' => 'A totes different item', + 'tax_amount' => 10, + 'unit_cost' => 50, + 'quantity' => 1 + } + ] + } + + assert response = @gateway.purchase(100, @visa_card, options) + assert_success response + assert_equal 'succeeded', response.params['status'] + assert response.params.dig('charges', 'data')[0]['captured'] end def test_unsuccessful_purchase_google_pay_with_invalid_card_number @@ -151,6 +211,45 @@ def test_successful_authorize_with_google_pay assert_match('google_pay', auth.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type']) end + def test_successful_purchase_with_google_pay + options = { + currency: 'GBP' + } + + purchase = @gateway.purchase(@amount, @google_pay, options) + + assert_match('android_pay', purchase.responses.first.params.dig('token', 'card', 'tokenization_method')) + assert purchase.success? + assert_match('google_pay', purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type']) + end + + def test_successful_purchase_with_tokenized_visa + options = { + currency: 'USD', + last_4: '4242' + } + + purchase = @gateway.purchase(@amount, @network_token_credit_card, options) + assert_equal(nil, purchase.responses.first.params.dig('token', 'card', 'tokenization_method')) + assert purchase.success? + assert_not_nil(purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['network_token']) + end + + def test_successful_purchase_with_google_pay_when_sending_the_billing_address + options = { + currency: 'GBP', + billing_address: address + } + + purchase = @gateway.purchase(@amount, @google_pay, options) + + assert_match('android_pay', purchase.responses.first.params.dig('token', 'card', 'tokenization_method')) + billing_address_line1 = purchase.responses.first.params.dig('token', 'card', 'address_line1') + assert_equal '456 My Street', billing_address_line1 + assert purchase.success? + assert_match('google_pay', purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type']) + end + def test_successful_purchase_with_apple_pay options = { currency: 'GBP' @@ -162,6 +261,20 @@ def test_successful_purchase_with_apple_pay assert_match('apple_pay', purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type']) end + def test_successful_purchase_with_apple_pay_when_sending_the_billing_address + options = { + currency: 'GBP', + billing_address: address + } + + purchase = @gateway.purchase(@amount, @apple_pay, options) + assert_match('apple_pay', purchase.responses.first.params.dig('token', 'card', 'tokenization_method')) + billing_address_line1 = purchase.responses.first.params.dig('token', 'card', 'address_line1') + assert_equal '456 My Street', billing_address_line1 + assert purchase.success? + assert_match('apple_pay', purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type']) + end + def test_succesful_purchase_with_connect_for_apple_pay options = { stripe_account: @destination_account @@ -516,6 +629,41 @@ def test_create_setup_intent_with_setup_future_usage end end + def test_create_setup_intent_with_connected_account + [@three_ds_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + assert authorize_response = @gateway.create_setup_intent(card_to_use, { + address: { + email: 'test@example.com', + name: 'John Doe', + line1: '1 Test Ln', + city: 'Durham', + tracking_number: '123456789' + }, + currency: 'USD', + confirm: true, + execute_threed: true, + return_url: 'https://example.com', + stripe_account: @destination_account + }) + + assert_equal 'requires_action', authorize_response.params['status'] + assert_match 'https://hooks.stripe.com', authorize_response.params.dig('next_action', 'redirect_to_url', 'url') + + # since we cannot "click" the stripe hooks URL to confirm the authorization + # we will at least confirm we can retrieve the created setup_intent and it contains the structure we expect + setup_intent_id = authorize_response.params['id'] + + # If we did not pass the stripe_account header it would return an error + assert si_response = @gateway.retrieve_setup_intent(setup_intent_id, { + stripe_account: @destination_account + }) + assert_equal 'requires_action', si_response.params['status'] + + assert_not_empty si_response.params.dig('latest_attempt', 'payment_method_details', 'card') + assert_nil si_response.params.dig('latest_attempt', 'payment_method_details', 'card', 'network_transaction_id') + end + end + def test_create_setup_intent_with_request_three_d_secure [@three_ds_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| assert authorize_response = @gateway.create_setup_intent(card_to_use, { @@ -531,6 +679,7 @@ def test_create_setup_intent_with_request_three_d_secure execute_threed: true, return_url: 'https://example.com', request_three_d_secure: 'any' + }) assert_equal 'requires_action', authorize_response.params['status'] @@ -631,7 +780,7 @@ def test_purchase_works_with_stored_credentials_without_optional_ds_transaction_ confirm: true, off_session: true, stored_credential: { - network_transaction_id: '1098510912210968', # TEST env seems happy with any value :/ + network_transaction_id: '1098510912210968' # TEST env seems happy with any value :/ } }) @@ -661,6 +810,77 @@ def test_succeeds_with_ntid_in_stored_credentials_and_separately end end + def test_succeeds_with_initial_cit + assert purchase = @gateway.purchase(@amount, @visa_card, { + currency: 'USD', + execute_threed: true, + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'cardholder', + reason_type: 'unscheduled', + initial_transaction: true + } + }) + assert_success purchase + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + assert purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['network_transaction_id'] + end + + def test_succeeds_with_initial_cit_3ds_required + assert purchase = @gateway.purchase(@amount, @three_ds_authentication_required_setup_for_off_session, { + currency: 'USD', + execute_threed: true, + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'cardholder', + reason_type: 'unscheduled', + initial_transaction: true + } + }) + assert_success purchase + assert_equal 'requires_action', purchase.params['status'] + end + + def test_succeeds_with_mit + assert purchase = @gateway.purchase(@amount, @visa_card, { + currency: 'USD', + execute_threed: true, + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'merchant', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: '1098510912210968' + } + }) + assert_success purchase + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + assert purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['network_transaction_id'] + end + + def test_succeeds_with_mit_3ds_required + assert purchase = @gateway.purchase(@amount, @three_ds_authentication_required_setup_for_off_session, { + currency: 'USD', + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'merchant', + reason_type: 'unscheduled', + initial_transaction: false, + network_transaction_id: '1098510912210968' + } + }) + assert_success purchase + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + assert purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['network_transaction_id'] + end + def test_successful_off_session_purchase_when_claim_without_transaction_id_present [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| assert response = @gateway.purchase(@amount, card_to_use, { @@ -715,7 +935,7 @@ def test_purchase_fails_on_unexpected_3ds_initiation assert response = @gateway.purchase(100, @three_ds_credit_card, options) assert_failure response - assert_match 'Received unexpected 3DS authentication response', response.message + assert_match 'Received unexpected 3DS authentication response, but a 3DS initiation flag was not included in the request.', response.message end def test_create_payment_intent_with_shipping_address @@ -740,13 +960,14 @@ def test_create_payment_intent_with_billing_address currency: 'USD', customer: @customer, billing_address: address, + email: 'jim@widgets.inc', confirm: true } - assert response = @gateway.create_intent(@amount, @visa_card, options) assert_success response - assert billing = response.params.dig('charges', 'data')[0].dig('billing_details', 'address') - assert_equal 'Ottawa', billing['city'] + assert billing_details = response.params.dig('charges', 'data')[0].dig('billing_details') + assert_equal 'Ottawa', billing_details['address']['city'] + assert_equal 'jim@widgets.inc', billing_details['email'] end def test_create_payment_intent_with_name_if_billing_address_absent @@ -831,6 +1052,24 @@ def test_create_a_payment_intent_and_manually_capture assert_equal 'Payment complete.', capture_response.params.dig('charges', 'data')[0].dig('outcome', 'seller_message') end + def test_create_a_payment_intent_and_manually_capture_with_network_token + options = { + currency: 'GBP', + customer: @customer, + confirmation_method: 'manual', + capture_method: 'manual', + confirm: true, + last_4: '4242' + } + assert create_response = @gateway.create_intent(@amount, @network_token_credit_card, options) + intent_id = create_response.params['id'] + assert_equal 'requires_capture', create_response.params['status'] + + assert capture_response = @gateway.capture(@amount, intent_id, options) + assert_equal 'succeeded', capture_response.params['status'] + assert_equal 'Payment complete.', capture_response.params.dig('charges', 'data')[0].dig('outcome', 'seller_message') + end + def test_failed_create_a_payment_intent_with_set_error_on_requires_action options = { currency: 'GBP', @@ -1097,6 +1336,21 @@ def test_successful_store_with_idempotency_key assert_equal store1.params['id'], store2.params['id'] end + def test_successful_customer_creating + options = { + currency: 'GBP', + billing_address: address, + shipping_address: address.merge!(email: 'test@email.com') + } + assert customer = @gateway.customer({}, @visa_card, options) + + assert_equal customer.params['name'], 'Jim Smith' + assert_equal customer.params['phone'], '(555)555-5555' + assert_nil customer.params['shipping']['email'] + assert_not_empty customer.params['shipping'] + assert_not_empty customer.params['address'] + end + def test_successful_store_with_false_validate_option options = { currency: 'GBP', @@ -1121,7 +1375,8 @@ def test_successful_verify options = { customer: @customer } - assert verify = @gateway.verify(@visa_payment_method, options) + assert verify = @gateway.verify(@visa_card, options) + assert_equal 'US', verify.responses[0].params.dig('card', 'country') assert_equal 'succeeded', verify.params['status'] end @@ -1134,6 +1389,13 @@ def test_failed_verify assert_equal 'Your card was declined.', verify.message end + def test_verify_stores_response_for_payment_method_creation + assert verify = @gateway.verify(@visa_card) + + assert_equal 2, verify.responses.count + assert_match 'pm_', verify.responses.first.params['id'] + end + def test_moto_enabled_card_requires_action_when_not_marked options = { currency: 'GBP', diff --git a/test/remote/gateways/remote_stripe_test.rb b/test/remote/gateways/remote_stripe_test.rb index 3722206a8f9..85417f3c583 100644 --- a/test/remote/gateways/remote_stripe_test.rb +++ b/test/remote/gateways/remote_stripe_test.rb @@ -131,6 +131,33 @@ def test_successful_purchase_with_level3_data assert_equal 'wow@example.com', response.params['metadata']['email'] end + def test_successful_purchase_with_shipping_address + @options[:shipping_address] = {} + @options[:shipping_address][:name] = 'Jim Doe' + @options[:shipping_address][:phone_number] = '9194041014' + @options[:shipping_address][:address1] = '100 W Main St' + @options[:shipping_address][:address2] = 'Apt 2' + @options[:shipping_address][:city] = 'Baltimore' + @options[:shipping_address][:state] = 'MD' + @options[:shipping_address][:zip] = '21201' + @options[:shipping_address][:country] = 'US' + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'charge', response.params['object'] + assert_equal response.authorization, response.params['id'] + assert response.params['paid'] + assert_equal 'ActiveMerchant Test Purchase', response.params['description'] + assert_equal 'Jim Doe', response.params['shipping']['name'] + assert_equal '9194041014', response.params['shipping']['phone'] + assert_equal '100 W Main St', response.params['shipping']['address']['line1'] + assert_equal 'Apt 2', response.params['shipping']['address']['line2'] + assert_equal 'Baltimore', response.params['shipping']['address']['city'] + assert_equal 'MD', response.params['shipping']['address']['state'] + assert_equal '21201', response.params['shipping']['address']['postal_code'] + assert_equal 'US', response.params['shipping']['address']['country'] + end + def test_purchase_with_connected_account destination = fixtures(:stripe_destination)[:stripe_user_id] transfer_group = 'XFERGROUP' @@ -316,6 +343,19 @@ def test_successful_void_with_reason assert_equal 'fraudulent', void.params['reason'] end + def test_successful_void_with_reverse_transfer + destination = fixtures(:stripe_destination)[:stripe_user_id] + assert response = @gateway.authorize(@amount, @credit_card, @options.merge(destination: destination)) + assert_success response + + @gateway.capture(@amount, response.authorization) + + assert void = @gateway.void(response.authorization, reverse_transfer: true) + assert_match %r{trr_}, void.params['transfer_reversal'] + assert_success void + assert_equal 'Transaction approved', void.message + end + def test_unsuccessful_void assert void = @gateway.void('active_merchant_fake_charge') assert_failure void @@ -622,7 +662,7 @@ def test_invalid_login # These "track data present" tests fail with invalid expiration dates. The # test track data probably needs to be updated. def test_card_present_purchase - @credit_card.track_data = '%B378282246310005^LONGSON/LONGBOB^2205101130504392?' + @credit_card.track_data = '%B378282246310005^LONGSON/LONGBOB^2705101130504392?' assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'charge', response.params['object'] @@ -630,7 +670,7 @@ def test_card_present_purchase end def test_card_present_authorize_and_capture - @credit_card.track_data = '%B378282246310005^LONGSON/LONGBOB^2205101130504392?' + @credit_card.track_data = '%B378282246310005^LONGSON/LONGBOB^2705101130504392?' assert authorization = @gateway.authorize(@amount, @credit_card, @options) assert_success authorization refute authorization.params['captured'] diff --git a/test/remote/gateways/remote_sum_up_test.rb b/test/remote/gateways/remote_sum_up_test.rb new file mode 100644 index 00000000000..e9bc8d9582c --- /dev/null +++ b/test/remote/gateways/remote_sum_up_test.rb @@ -0,0 +1,156 @@ +require 'test_helper' + +class RemoteSumUpTest < Test::Unit::TestCase + def setup + @gateway = SumUpGateway.new(fixtures(:sum_up)) + + @amount = 100 + @credit_card = credit_card('4000100011112224') + @declined_card = credit_card('55555555555555555') + @options = { + payment_type: 'card', + billing_address: address, + description: 'Store Purchase', + order_id: SecureRandom.uuid + } + end + + def test_handle_credentials_error + gateway = SumUpGateway.new({ access_token: 'sup_sk_xx', pay_to_email: 'example@example.com' }) + response = gateway.purchase(@amount, @visa_card, @options) + + assert_equal('invalid access token', response.message) + end + + def test_handle_pay_to_email_credential_error + gateway = SumUpGateway.new(fixtures(:sum_up).merge(pay_to_email: 'example@example.com')) + response = gateway.purchase(@amount, @visa_card, @options) + + assert_equal('Validation error', response.message) + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'PENDING', response.message + assert_equal @options[:order_id], response.params['checkout_reference'] + refute_empty response.params['id'] + refute_empty response.params['transactions'] + refute_empty response.params['transactions'].first['id'] + assert_equal 'PENDING', response.params['transactions'].first['status'] + end + + def test_successful_purchase_with_existing_checkout + existing_checkout = @gateway.purchase(@amount, @credit_card, @options) + assert_success existing_checkout + refute_empty existing_checkout.params['id'] + @options[:checkout_id] = existing_checkout.params['id'] + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'PENDING', response.message + assert_equal @options[:order_id], response.params['checkout_reference'] + refute_empty response.params['id'] + assert_equal existing_checkout.params['id'], response.params['id'] + refute_empty response.params['transactions'] + assert_equal response.params['transactions'].count, 2 + refute_empty response.params['transactions'].last['id'] + assert_equal 'PENDING', response.params['transactions'].last['status'] + end + + def test_successful_purchase_with_more_options + options = { + email: 'joe@example.com', + tax_id: '12345', + redirect_url: 'https://checkout.example.com', + return_url: 'https://checkout.example.com', + billing_address: address, + order_id: SecureRandom.uuid, + currency: 'USD', + description: 'Sample description', + payment_type: 'card' + } + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'PENDING', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_equal 'Validation error', response.message + assert_equal 'The value located under the \'$.card.number\' path is not a valid card number', response.params['detail'] + end + + def test_failed_purchase_invalid_customer_id + options = @options.merge!(customer_id: 'customer@example.com', payment_type: 'card') + response = @gateway.purchase(@amount, @credit_card, options) + assert_failure response + assert_equal 'Validation error', response.message + assert_equal 'customer_id', response.params['param'] + end + + def test_failed_purchase_invalid_currency + options = @options.merge!(currency: 'EUR') + response = @gateway.purchase(@amount, @credit_card, options) + assert_failure response + assert_equal 'Given currency differs from merchant\'s country currency', response.message + end + + def test_successful_void + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + assert_equal 'PENDING', purchase.message + assert_equal @options[:order_id], purchase.params['checkout_reference'] + refute_empty purchase.params['id'] + refute_empty purchase.params['transactions'] + refute_empty purchase.params['transactions'].first['id'] + assert_equal 'PENDING', purchase.params['transactions'].first['status'] + + response = @gateway.void(purchase.params['id']) + assert_success response + refute_empty response.params['id'] + assert_equal purchase.params['id'], response.params['id'] + refute_empty response.params['transactions'] + refute_empty response.params['transactions'].first['id'] + assert_equal 'CANCELLED', response.params['transactions'].first['status'] + end + + def test_failed_void_invalid_checkout_id + response = @gateway.void('90858be3-23bb-4af5-9fba-ce3bc190fe5b22') + assert_failure response + assert_equal 'Resource not found', response.message + end + + def test_failed_refund_for_pending_checkout + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + assert_equal 'PENDING', purchase.message + assert_equal @options[:order_id], purchase.params['checkout_reference'] + refute_empty purchase.params['id'] + refute_empty purchase.params['transactions'] + + transaction_id = purchase.params['transactions'].first['id'] + + refute_empty transaction_id + assert_equal 'PENDING', purchase.params['transactions'].first['status'] + + response = @gateway.refund(nil, transaction_id) + assert_failure response + assert_equal 'CONFLICT', response.error_code + assert_equal 'The transaction is not refundable in its current state', response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + assert_scrubbed(@gateway.options[:pay_to_email], transcript) + end +end diff --git a/test/remote/gateways/remote_tns_test.rb b/test/remote/gateways/remote_tns_test.rb index 6155f16f5fa..f515c9854ff 100644 --- a/test/remote/gateways/remote_tns_test.rb +++ b/test/remote/gateways/remote_tns_test.rb @@ -6,8 +6,8 @@ def setup @gateway = TnsGateway.new(fixtures(:tns)) @amount = 100 - @credit_card = credit_card('5123456789012346', month: 05, year: 2021) - @ap_credit_card = credit_card('5424180279791732', month: 05, year: 2021) + @credit_card = credit_card('5123456789012346', month: 05, year: 2024) + @ap_credit_card = credit_card('5424180279791732', month: 05, year: 2024) @declined_card = credit_card('5123456789012346', month: 01, year: 2028) @options = { @@ -67,7 +67,7 @@ def test_successful_purchase_with_region def test_failed_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'FAILURE - DECLINED', response.message + assert_equal 'FAILURE - UNSPECIFIED_FAILURE', response.message end def test_successful_authorize_and_capture @@ -84,7 +84,7 @@ def test_successful_authorize_and_capture def test_failed_authorize assert response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_equal 'FAILURE - DECLINED', response.message + assert_equal 'FAILURE - UNSPECIFIED_FAILURE', response.message end def test_successful_refund @@ -134,11 +134,4 @@ def test_transcript_scrubbing assert_scrubbed(card.verification_value, transcript) assert_scrubbed(@gateway.options[:password], transcript) end - - def test_verify_credentials - assert @gateway.verify_credentials - - gateway = TnsGateway.new(userid: 'unknown', password: 'unknown') - assert !gateway.verify_credentials - end end diff --git a/test/remote/gateways/remote_trust_commerce_test.rb b/test/remote/gateways/remote_trust_commerce_test.rb index 78587a190c6..19e1564c649 100644 --- a/test/remote/gateways/remote_trust_commerce_test.rb +++ b/test/remote/gateways/remote_trust_commerce_test.rb @@ -182,6 +182,13 @@ def test_failed_store assert_bad_data_response(response) end + def test_successful_unstore + assert store = @gateway.store(@credit_card) + assert_equal 'approved', store.params['status'] + assert response = @gateway.unstore(store.params['billingid']) + assert_success response + end + def test_unstore_failure assert response = @gateway.unstore('does-not-exist') @@ -189,6 +196,28 @@ def test_unstore_failure assert_failure response end + def test_successful_purchase_after_store + assert store = @gateway.store(@credit_card) + assert_success store + assert response = @gateway.purchase(@amount, store.params['billingid'], @options) + assert_equal 'Y', response.avs_result['code'] + assert_match %r{The transaction was successful}, response.message + end + + def test_successful_verify + assert response = @gateway.verify(@credit_card) + assert_equal 'approved', response.params['status'] + assert_match %r{The transaction was successful}, response.message + assert_success response + end + + def test_failed_verify_with_invalid_card + assert response = @gateway.verify(@declined_credit_card) + assert_equal 'baddata', response.params['status'] + assert_match %r{A field was improperly formatted}, response.message + assert_failure response + end + def test_successful_recurring assert response = @gateway.recurring(@amount, @credit_card, periodicity: :weekly) diff --git a/test/remote/gateways/remote_vanco_test.rb b/test/remote/gateways/remote_vanco_test.rb index 51b8988289a..c35cdf02ad9 100644 --- a/test/remote/gateways/remote_vanco_test.rb +++ b/test/remote/gateways/remote_vanco_test.rb @@ -1,6 +1,8 @@ require 'test_helper' class RemoteVancoTest < Test::Unit::TestCase + SECONDS_PER_DAY = 3600 * 24 + def setup @gateway = VancoGateway.new(fixtures(:vanco)) @@ -34,6 +36,42 @@ def test_successful_purchase_with_ip_address assert_equal 'Success', response.message end + def test_successful_purchase_with_existing_session_id + previous_login_response = @gateway.send(:login) + + response = @gateway.purchase( + @amount, + @credit_card, + @options.merge( + session_id: + { + id: previous_login_response.params['response_sessionid'], + created_at: Time.parse(previous_login_response.params['auth_requesttime']) + } + ) + ) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_expired_session_id + two_days_from_now = Time.now - (2 * SECONDS_PER_DAY) + previous_login_response = @gateway.send(:login) + + response = @gateway.purchase( + @amount, + @credit_card, + @options.merge( + session_id: { + id: previous_login_response.params['response_sessionid'], + created_at: two_days_from_now + } + ) + ) + assert_success response + assert_equal 'Success', response.message + end + def test_successful_purchase_sans_minimal_options response = @gateway.purchase(@amount, @credit_card) assert_success response diff --git a/test/remote/gateways/remote_vpos_test.rb b/test/remote/gateways/remote_vpos_test.rb index b0e99618838..4a75f8d1754 100644 --- a/test/remote/gateways/remote_vpos_test.rb +++ b/test/remote/gateways/remote_vpos_test.rb @@ -5,7 +5,7 @@ def setup @gateway = VposGateway.new(fixtures(:vpos)) @amount = 100000 - @credit_card = credit_card('5418630110000014', month: 8, year: 2021, verification_value: '258') + @credit_card = credit_card('5418630110000014', month: 8, year: 2026, verification_value: '277') @declined_card = credit_card('4000300011112220') @options = { billing_address: address, @@ -95,7 +95,7 @@ def test_failed_void end def test_invalid_login - gateway = VposGateway.new(private_key: '', public_key: '', commerce: 123, commerce_branch: 45) + gateway = VposGateway.new(private_key: '', public_key: '', encryption_key: OpenSSL::PKey::RSA.new(512), commerce: 123, commerce_branch: 45) response = gateway.void('') assert_failure response diff --git a/test/remote/gateways/remote_vpos_without_key_test.rb b/test/remote/gateways/remote_vpos_without_key_test.rb new file mode 100644 index 00000000000..a17e98838f2 --- /dev/null +++ b/test/remote/gateways/remote_vpos_without_key_test.rb @@ -0,0 +1,125 @@ +require 'test_helper' + +class RemoteVposWithoutKeyTest < Test::Unit::TestCase + def setup + vpos_fixtures = fixtures(:vpos) + vpos_fixtures.delete(:encryption_key) + @gateway = VposGateway.new(vpos_fixtures) + + @amount = 100000 + @credit_card = credit_card('5418630110000014', month: 8, year: 2026, verification_value: '277') + @declined_card = credit_card('4000300011112220') + @options = { + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Transaccion aprobada', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_equal 'IMPORTE DE LA TRN INFERIOR AL M¿NIMO PERMITIDO', response.message + end + + def test_successful_refund_using_auth + shop_process_id = SecureRandom.random_number(10**15) + + assert purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + authorization = purchase.authorization + + assert refund = @gateway.refund(@amount, authorization, @options.merge(shop_process_id: shop_process_id)) + assert_success refund + assert_equal 'Transaccion aprobada', refund.message + end + + def test_successful_refund_using_shop_process_id + shop_process_id = SecureRandom.random_number(10**15) + + assert purchase = @gateway.purchase(@amount, @credit_card, @options.merge(shop_process_id: shop_process_id)) + assert_success purchase + + assert refund = @gateway.refund(@amount, nil, original_shop_process_id: shop_process_id) # 315300749110268, 21611732218038 + assert_success refund + assert_equal 'Transaccion aprobada', refund.message + end + + def test_successful_credit + assert credit = @gateway.credit(@amount, @credit_card) + assert_success credit + assert_equal 'Transaccion aprobada', credit.message + end + + def test_failed_credit + response = @gateway.credit(@amount, @declined_card) + assert_failure response + assert_equal 'RefundsServiceError:TIPO DE TRANSACCION NO PERMITIDA PARA TARJETAS EXTRANJERAS', response.message + end + + def test_successful_void + shop_process_id = SecureRandom.random_number(10**15) + options = @options.merge({ shop_process_id: shop_process_id }) + + purchase = @gateway.purchase(@amount, @credit_card, options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization, options) + assert_success void + assert_equal 'RollbackSuccessful:Transacción Aprobada', void.message + end + + def test_duplicate_void_fails + shop_process_id = SecureRandom.random_number(10**15) + options = @options.merge({ shop_process_id: shop_process_id }) + + purchase = @gateway.purchase(@amount, @credit_card, options) + assert_success purchase + + assert void = @gateway.void(purchase.authorization, options) + assert_success void + assert_equal 'RollbackSuccessful:Transacción Aprobada', void.message + + assert duplicate_void = @gateway.void(purchase.authorization, options) + assert_failure duplicate_void + assert_equal 'AlreadyRollbackedError:The payment has already been rollbacked.', duplicate_void.message + end + + def test_failed_void + response = @gateway.void('abc#123') + assert_failure response + assert_equal 'BuyNotFoundError:Business Error', response.message + end + + def test_invalid_login + gateway = VposGateway.new(private_key: '', public_key: '', encryption_key: OpenSSL::PKey::RSA.new(512), commerce: 123, commerce_branch: 45) + + response = gateway.void('') + assert_failure response + assert_match %r{InvalidPublicKeyError}, response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + # does not contain anything other than '[FILTERED]' + assert_no_match(/token\\":\\"[^\[FILTERED\]]/, transcript) + assert_no_match(/card_encrypted_data\\":\\"[^\[FILTERED\]]/, transcript) + end + + def test_regenerate_encryption_key + puts 'Regenerating encryption key.' + puts 'Before running the standard vpos remote test suite, run this test individually:' + puts '$ ruby -Ilib:test test/remote/gateways/remote_vpos_without_key_test.rb -n test_regenerate_encryption_key' + puts 'Then copy this key into your fixtures file.' + p @gateway.one_time_public_key + end +end diff --git a/test/remote/gateways/remote_wompi_test.rb b/test/remote/gateways/remote_wompi_test.rb index ad0b3fbec20..65c312a1153 100644 --- a/test/remote/gateways/remote_wompi_test.rb +++ b/test/remote/gateways/remote_wompi_test.rb @@ -78,16 +78,16 @@ def test_partial_refund purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase - assert refund = @gateway.refund(@amount - 1, purchase.authorization) + assert refund = @gateway.refund(@amount - 50000, purchase.authorization) assert_success refund end - def test_failed_refund - response = @gateway.refund(@amount, '') - assert_failure response - message = JSON.parse(response.message) - assert_equal 'transaction_id Debe ser completado', message['transaction_id'].first - end + # def test_failed_refund + # response = @gateway.refund(@amount, '') + # assert_failure response + # message = JSON.parse(response.message) + # assert_equal 'transaction_id Debe ser completado', message['transaction_id'].first + # end def test_successful_void purchase = @gateway.purchase(@amount, @credit_card, @options) diff --git a/test/remote/gateways/remote_worldpay_test.rb b/test/remote/gateways/remote_worldpay_test.rb index 5c782af6bd7..585c554b700 100644 --- a/test/remote/gateways/remote_worldpay_test.rb +++ b/test/remote/gateways/remote_worldpay_test.rb @@ -6,38 +6,121 @@ def setup @cftgateway = WorldpayGateway.new(fixtures(:world_pay_gateway_cft)) @amount = 100 + @year = (Time.now.year + 2).to_s[-2..-1].to_i @credit_card = credit_card('4111111111111111') @amex_card = credit_card('3714 496353 98431') - @elo_credit_card = credit_card('4514 1600 0000 0008', + @elo_credit_card = credit_card( + '4514 1600 0000 0008', month: 10, year: 2020, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'elo') - @credit_card_with_two_digits_year = credit_card('4111111111111111', + brand: 'elo' + ) + @credit_card_with_two_digits_year = credit_card( + '4111111111111111', month: 10, - year: 22) + year: @year + ) @cabal_card = credit_card('6035220000000006') @naranja_card = credit_card('5895620000000002') @sodexo_voucher = credit_card('6060704495764400', brand: 'sodexo') @declined_card = credit_card('4111111111111111', first_name: nil, last_name: 'REFUSED') @threeDS_card = credit_card('4111111111111111', first_name: nil, last_name: 'doot') @threeDS2_card = credit_card('4111111111111111', first_name: nil, last_name: '3DS_V2_FRICTIONLESS_IDENTIFIED') + @threeDS2_challenge_card = credit_card('4000000000001091', first_name: nil, last_name: 'challenge-me-plz') @threeDS_card_external_MPI = credit_card('4444333322221111', first_name: 'AA', last_name: 'BD') - @nt_credit_card = network_tokenization_credit_card('4895370015293175', + @nt_credit_card = network_tokenization_credit_card( + '4895370015293175', brand: 'visa', eci: '07', source: :network_token, - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') - @nt_credit_card_without_eci = network_tokenization_credit_card('4895370015293175', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @nt_credit_card_without_eci = network_tokenization_credit_card( + '4895370015293175', source: :network_token, - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) @options = { order_id: generate_unique_id, email: 'wow@example.com' } + + @level_two_data = { + level_2_data: { + invoice_reference_number: 'INV12233565', + customer_reference: 'CUST00000101', + card_acceptor_tax_id: 'VAT1999292', + sales_tax: { + amount: '20', + exponent: '2', + currency: 'USD' + } + } + } + + @level_three_data = { + level_3_data: { + customer_reference: 'CUST00000102', + card_acceptor_tax_id: 'VAT1999285', + sales_tax: { + amount: '20', + exponent: '2', + currency: 'USD' + }, + discount_amount: { + amount: '1', + exponent: '2', + currency: 'USD' + }, + shipping_amount: { + amount: '50', + exponent: '2', + currency: 'USD' + }, + duty_amount: { + amount: '20', + exponent: '2', + currency: 'USD' + }, + item: { + description: 'Laptop 14', + product_code: 'LP00125', + commodity_code: 'COM00125', + quantity: '2', + unit_cost: { + amount: '1500', + exponent: '2', + currency: 'USD' + }, + unit_of_measure: 'each', + item_total: { + amount: '3000', + exponent: '2', + currency: 'USD' + }, + item_total_with_tax: { + amount: '3500', + exponent: '2', + currency: 'USD' + }, + item_discount_amount: { + amount: '200', + exponent: '2', + currency: 'USD' + }, + tax_amount: { + amount: '500', + exponent: '2', + currency: 'USD' + } + } + } + } + @store_options = { customer: generate_unique_id, email: 'wow@example.com' @@ -56,7 +139,8 @@ def setup sub_tax_id: '987-65-4321' } } - @apple_pay_network_token = network_tokenization_credit_card('4895370015293175', + @apple_pay_network_token = network_tokenization_credit_card( + '4895370015293175', month: 10, year: Time.new.year + 2, first_name: 'John', @@ -65,15 +149,18 @@ def setup payment_cryptogram: 'abc1234567890', eci: '07', transaction_id: 'abc123', - source: :apple_pay) + source: :apple_pay + ) - @google_pay_network_token = network_tokenization_credit_card('4444333322221111', + @google_pay_network_token = network_tokenization_credit_card( + '4444333322221111', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', month: '01', year: Time.new.year + 2, source: :google_pay, transaction_id: '123456789', - eci: '05') + eci: '05' + ) end def test_successful_purchase @@ -367,7 +454,6 @@ def test_successful_authorize_with_3ds } ) assert first_message = @gateway.authorize(@amount, @threeDS_card, options) - assert_equal "A transaction status of 'AUTHORISED' or 'CAPTURED' is required.", first_message.message assert first_message.test? refute first_message.authorization.blank? refute first_message.params['issuer_url'].blank? @@ -383,6 +469,26 @@ def test_marking_3ds_purchase_as_moto assert_equal 'SUCCESS', response.message end + def test_successful_authorize_with_3ds2_challenge + session_id = generate_unique_id + options = @options.merge( + { + execute_threed: true, + accept_header: 'text/html', + user_agent: 'Mozilla/5.0', + session_id: session_id, + ip: '127.0.0.1' + } + ) + assert response = @gateway.authorize(@amount, @threeDS2_challenge_card, options) + assert response.test? + refute response.authorization.blank? + refute response.params['issuer_url'].blank? + refute response.params['pa_request'].blank? + refute response.params['cookie'].blank? + refute response.params['session_id'].blank? + end + def test_successful_auth_and_capture_with_normalized_stored_credential stored_credential_params = { initial_transaction: true, @@ -462,7 +568,6 @@ def test_successful_authorize_with_3ds_with_normalized_stored_credentials } ) assert first_message = @gateway.authorize(@amount, @threeDS_card, options) - assert_equal "A transaction status of 'AUTHORISED' or 'CAPTURED' is required.", first_message.message assert first_message.test? refute first_message.authorization.blank? refute first_message.params['issuer_url'].blank? @@ -485,7 +590,6 @@ def test_successful_authorize_with_3ds_with_gateway_specific_stored_credentials } ) assert first_message = @gateway.authorize(@amount, @threeDS_card, options) - assert_equal "A transaction status of 'AUTHORISED' or 'CAPTURED' is required.", first_message.message assert first_message.test? refute first_message.authorization.blank? refute first_message.params['issuer_url'].blank? @@ -494,6 +598,44 @@ def test_successful_authorize_with_3ds_with_gateway_specific_stored_credentials refute first_message.params['session_id'].blank? end + def test_successful_purchase_with_level_two_fields + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_two_data)) + assert_success response + assert_equal true, response.params['ok'] + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_level_two_fields_and_sales_tax_zero + @level_two_data[:level_2_data][:sales_tax][:amount] = 0 + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_two_data)) + assert_success response + assert_equal true, response.params['ok'] + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_level_three_fields + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_three_data)) + assert_success response + assert_equal true, response.params['ok'] + assert_equal 'SUCCESS', response.message + end + + def test_unsuccessful_purchase_level_three_data_without_item_mastercard + @level_three_data[:level_3_data][:item] = {} + @credit_card.brand = 'master' + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_three_data)) + assert_failure response + assert_equal response.error_code, '2' + assert_equal response.params['error'].gsub(/\"+/, ''), 'The content of element type item is incomplete, it must match (description,productCode?,commodityCode?,quantity?,unitCost?,unitOfMeasure?,itemTotal?,itemTotalWithTax?,itemDiscountAmount?,taxAmount?,categories?,pageURL?,imageURL?).' + end + + def test_successful_purchase_with_level_two_and_three_fields + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_two_data, @level_three_data)) + assert_success response + assert_equal true, response.params['ok'] + assert_equal 'SUCCESS', response.message + end + # Fails currently because the sandbox doesn't actually validate the stored_credential options # def test_failed_authorize_with_bad_stored_cred_options # assert auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential_usage: 'FIRST')) @@ -721,7 +863,7 @@ def test_successful_fast_fund_credit_with_token_on_cft_gateway assert_success store options = @options.merge({ fast_fund_credit: true }) - assert credit = @gateway.credit(@amount, store.authorization, options) + assert credit = @cftgateway.credit(@amount, store.authorization, options) assert_success credit end @@ -1027,6 +1169,31 @@ def test_successful_purchase_with_options_synchronous_response assert_success purchase end + # There is a delay of up to 5 minutes for a transaction to be recorded by Worldpay. Inquiring + # too soon will result in an error "Order not ready". Leaving commented out due to included sleeps. + # def test_successful_inquire_with_order_id + # order_id = @options[:order_id] + # assert auth = @gateway.authorize(@amount, @credit_card, @options) + # assert_success auth + # assert auth.authorization + # sleep 60 + + # assert inquire = @gateway.inquire(nil, { order_id: order_id }) + # assert_success inquire + # assert auth.authorization == inquire.authorization + # end + + # def test_successful_inquire_with_authorization + # assert auth = @gateway.authorize(@amount, @credit_card, @options) + # assert_success auth + # assert auth.authorization + # sleep 60 + + # assert inquire = @gateway.inquire(auth.authorization, {}) + # assert_success inquire + # assert auth.authorization == inquire.authorization + # end + private def risk_data diff --git a/test/remote/gateways/remote_xpay_test.rb b/test/remote/gateways/remote_xpay_test.rb new file mode 100644 index 00000000000..92ec7c33ab5 --- /dev/null +++ b/test/remote/gateways/remote_xpay_test.rb @@ -0,0 +1,15 @@ +require 'test_helper' + +class RemoteRapydTest < Test::Unit::TestCase + def setup + @gateway = XpayGateway.new(fixtures(:x_pay)) + @amount = 200 + @credit_card = credit_card('4111111111111111') + @options = {} + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert response + end +end diff --git a/test/schema/cyber_source/CyberSourceTransaction_1.198.xsd b/test/schema/cyber_source/CyberSourceTransaction_1.198.xsd new file mode 100644 index 00000000000..aed30c01e3c --- /dev/null +++ b/test/schema/cyber_source/CyberSourceTransaction_1.198.xsd @@ -0,0 +1,5349 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/schema/cyber_source/CyberSourceTransaction_1.201.xsd b/test/schema/cyber_source/CyberSourceTransaction_1.201.xsd new file mode 100644 index 00000000000..ec16a9d7dcd --- /dev/null +++ b/test/schema/cyber_source/CyberSourceTransaction_1.201.xsd @@ -0,0 +1,5106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index 44dfb10f62d..8c562336cea 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -151,6 +151,7 @@ def formatted_expiration_date(credit_card) end def credit_card(number = '4242424242424242', options = {}) + number = number.is_a?(Integer) ? number.to_s : number defaults = { number: number, month: default_expiration_date.month, @@ -213,10 +214,12 @@ def apple_pay_payment_token(options = {}) transaction_identifier: 'uniqueidentifier123' }.update(options) - ActiveMerchant::Billing::ApplePayPaymentToken.new(defaults[:payment_data], + ActiveMerchant::Billing::ApplePayPaymentToken.new( + defaults[:payment_data], payment_instrument_name: defaults[:payment_instrument_name], payment_network: defaults[:payment_network], - transaction_identifier: defaults[:transaction_identifier]) + transaction_identifier: defaults[:transaction_identifier] + ) end def address(options = {}) @@ -234,6 +237,19 @@ def address(options = {}) }.update(options) end + def shipping_address(options = {}) + { + name: 'Jon Smith', + address1: '123 Your Street', + address2: 'Apt 2', + city: 'Toronto', + state: 'ON', + zip: 'K2C3N7', + country: 'CA', + phone_number: '(123)456-7890' + }.update(options) + end + def statement_address(options = {}) { address1: '456 My Street', @@ -257,6 +273,7 @@ def stored_credential(*args, **options) stored_credential[:reason_type] = 'recurring' if args.include?(:recurring) stored_credential[:reason_type] = 'unscheduled' if args.include?(:unscheduled) stored_credential[:reason_type] = 'installment' if args.include?(:installment) + stored_credential[:reason_type] = 'internet' if args.include?(:internet) stored_credential[:initiator] = 'cardholder' if args.include?(:cardholder) stored_credential[:initiator] = 'merchant' if args.include?(:merchant) @@ -281,7 +298,7 @@ def fixtures(key) def load_fixtures [DEFAULT_CREDENTIALS, LOCAL_CREDENTIALS].inject({}) do |credentials, file_name| if File.exist?(file_name) - yaml_data = YAML.safe_load(File.read(file_name), [], [], true) + yaml_data = YAML.safe_load(File.read(file_name), aliases: true) credentials.merge!(symbolize_keys(yaml_data)) end credentials diff --git a/test/unit/check_test.rb b/test/unit/check_test.rb index f017ee131ad..bdd03346d1d 100644 --- a/test/unit/check_test.rb +++ b/test/unit/check_test.rb @@ -4,12 +4,35 @@ class CheckTest < Test::Unit::TestCase VALID_ABA = '111000025' INVALID_ABA = '999999999' MALFORMED_ABA = 'I like fish' - VALID_CBA = '000194611' + VALID_ELECTRONIC_CBA = '000194611' + VALID_MICR_CBA = '94611001' INVALID_NINE_DIGIT_CBA = '012345678' INVALID_SEVEN_DIGIT_ROUTING_NUMBER = '0123456' ACCOUNT_NUMBER = '123456789012' + CHECK_US = Check.new( + name: 'Fred Bloggs', + routing_number: VALID_ABA, + account_number: ACCOUNT_NUMBER, + account_holder_type: 'personal', + account_type: 'checking' + ) + CHECK_CAN_E_FORMAT = Check.new( + name: 'Tim Horton', + routing_number: VALID_ELECTRONIC_CBA, + account_number: ACCOUNT_NUMBER, + account_holder_type: 'personal', + account_type: 'checking' + ) + CHECK_CAN_MICR_FORMAT = Check.new( + name: 'Tim Horton', + routing_number: VALID_MICR_CBA, + account_number: ACCOUNT_NUMBER, + account_holder_type: 'personal', + account_type: 'checking' + ) + def test_validation assert_not_valid Check.new end @@ -29,13 +52,7 @@ def test_nil_name end def test_valid - assert_valid Check.new( - name: 'Fred Bloggs', - routing_number: VALID_ABA, - account_number: ACCOUNT_NUMBER, - account_holder_type: 'personal', - account_type: 'checking' - ) + assert_valid CHECK_US end def test_credit_card? @@ -82,14 +99,12 @@ def test_account_type assert !c.validate[:account_type] end - def test_valid_canada_routing_number - assert_valid Check.new( - name: 'Tim Horton', - routing_number: VALID_CBA, - account_number: ACCOUNT_NUMBER, - account_holder_type: 'personal', - account_type: 'checking' - ) + def test_valid_canada_routing_number_electronic_format + assert_valid CHECK_CAN_E_FORMAT + end + + def test_valid_canada_routing_number_micr_format + assert_valid CHECK_CAN_MICR_FORMAT end def test_invalid_canada_routing_number @@ -115,4 +130,14 @@ def test_invalid_routing_number_length assert_equal ['is invalid'], errors[:routing_number] end + + def test_format_routing_number + assert CHECK_CAN_E_FORMAT.micr_format_routing_number == '94611001' + assert CHECK_CAN_MICR_FORMAT.micr_format_routing_number == '94611001' + assert CHECK_US.micr_format_routing_number == '111000025' + + assert CHECK_CAN_E_FORMAT.electronic_format_routing_number == '000194611' + assert CHECK_CAN_MICR_FORMAT.electronic_format_routing_number == '000194611' + assert CHECK_US.electronic_format_routing_number == '111000025' + end end diff --git a/test/unit/connection_test.rb b/test/unit/connection_test.rb index 3ef0b493486..338718a99b4 100644 --- a/test/unit/connection_test.rb +++ b/test/unit/connection_test.rb @@ -87,13 +87,6 @@ def test_successful_delete_with_body_request assert_equal 'success', response.body end - def test_get_raises_argument_error_if_passed_data - assert_raises(ArgumentError) do - Net::HTTP.any_instance.expects(:start).returns(true) - @connection.request(:get, 'data', {}) - end - end - def test_request_raises_when_request_method_not_supported assert_raises(ArgumentError) do Net::HTTP.any_instance.expects(:start).returns(true) diff --git a/test/unit/credit_card_methods_test.rb b/test/unit/credit_card_methods_test.rb index 3bc930116eb..e4c1240131c 100644 --- a/test/unit/credit_card_methods_test.rb +++ b/test/unit/credit_card_methods_test.rb @@ -13,7 +13,7 @@ def maestro_card_numbers 6390000000000000 6390700000000000 6390990000000000 6761999999999999 6763000000000000 6799999999999999 5000330000000000 5811499999999999 5010410000000000 - 5010630000000000 5892440000000000 + 5010630000000000 5892440000000000 5016230000000000 ] end @@ -28,7 +28,7 @@ def non_maestro_card_numbers def maestro_bins %w[500032 500057 501015 501016 501018 501020 501021 501023 501024 501025 501026 501027 501028 501029 501038 501039 501040 501041 501043 501045 501047 501049 501051 501053 501054 501055 501056 501057 - 501058 501060 501061 501062 501063 501066 501067 501072 501075 501083 501087 + 501058 501060 501061 501062 501063 501066 501067 501072 501075 501083 501087 501623 501800 501089 501091 501092 501095 501104 501105 501107 501108 501500 501879 502000 502113 502301 503175 503645 503800 503670 504310 504338 504363 504533 504587 504620 504639 504656 504738 504781 504910 @@ -184,6 +184,14 @@ def test_should_detect_alia_card assert_equal 'alia', CreditCard.brand?('5058740000000000') end + def test_should_detect_mada_card + assert_equal 'mada', CreditCard.brand?('5043000000000000') + assert_equal 'mada', CreditCard.brand?('5852650000000000') + assert_equal 'mada', CreditCard.brand?('5888500000000000') + assert_equal 'mada', CreditCard.brand?('6361200000000000') + assert_equal 'mada', CreditCard.brand?('9682040000000000') + end + def test_alia_number_not_validated 10.times do number = rand(5058740000000001..5058749999999999).to_s @@ -196,6 +204,31 @@ def test_should_detect_confiable_card assert_equal 'confiable', CreditCard.brand?('5607180000000000') end + def test_should_detect_bp_plus_card + assert_equal 'bp_plus', CreditCard.brand?('70501 501021600 378') + assert_equal 'bp_plus', CreditCard.brand?('70502 111111111 111') + assert_equal 'bp_plus', CreditCard.brand?('7050 15605297 00114') + assert_equal 'bp_plus', CreditCard.brand?('7050 15546992 00062') + end + + def test_should_validate_bp_plus_card + assert_true CreditCard.valid_number?('70501 501021600 378') + assert_true CreditCard.valid_number?('7050 15605297 00114') + assert_true CreditCard.valid_number?('7050 15546992 00062') + assert_true CreditCard.valid_number?('7050 16150146 00110') + assert_true CreditCard.valid_number?('7050 16364764 00070') + + # numbers with invalid formats + assert_false CreditCard.valid_number?('7050_15546992_00062') + assert_false CreditCard.valid_number?('70501 55469920 0062') + assert_false CreditCard.valid_number?('70 501554699 200062') + + # numbers that are luhn-invalid + assert_false CreditCard.valid_number?('70502 111111111 111') + assert_false CreditCard.valid_number?('7050 16364764 00071') + assert_false CreditCard.valid_number?('7050 16364764 00072') + end + def test_confiable_number_not_validated 10.times do number = rand(5607180000000001..5607189999999999).to_s @@ -224,6 +257,61 @@ def test_should_detect_olimpica_card assert_equal 'olimpica', CreditCard.brand?('6368530000000000') end + def test_should_detect_sodexo_no_luhn_card + number1 = '5058645584812145' + number2 = '5058655584812145' + assert_equal 'sodexo', CreditCard.brand?(number1) + assert CreditCard.valid_number?(number1) + assert_equal 'sodexo', CreditCard.brand?(number2) + assert CreditCard.valid_number?(number2) + end + + def test_should_validate_sodexo_no_luhn_card + assert_true CreditCard.valid_number?('5058645584812145') + assert_false CreditCard.valid_number?('5058665584812110') + end + + def test_should_detect_passcard_card + assert_equal 'passcard', CreditCard.brand?('6280260025383009') + assert_equal 'passcard', CreditCard.brand?('6280260025383280') + assert_equal 'passcard', CreditCard.brand?('6280260025383298') + assert_equal 'passcard', CreditCard.brand?('6280260025383306') + assert_equal 'passcard', CreditCard.brand?('6280260025383314') + end + + def test_should_validate_passcard_card + assert_true CreditCard.valid_number?('6280260025383009') + # numbers with invalid formats + assert_false CreditCard.valid_number?('6280_26002538_0005') + # numbers that are luhn-invalid + assert_false CreditCard.valid_number?('6280260025380991') + end + + def test_should_detect_edenred_card + assert_equal 'edenred', CreditCard.brand?('6374830000000823') + assert_equal 'edenred', CreditCard.brand?('6374830000000799') + assert_equal 'edenred', CreditCard.brand?('6374830000000807') + assert_equal 'edenred', CreditCard.brand?('6374830000000815') + assert_equal 'edenred', CreditCard.brand?('6374830000000823') + end + + def test_should_validate_edenred_card + assert_true CreditCard.valid_number?('6374830000000369') + # numbers with invalid formats + assert_false CreditCard.valid_number?('6374 8300000 00369') + # numbers that are luhn-invalid + assert_false CreditCard.valid_number?('6374830000000111') + end + + def test_should_detect_anda_card + assert_equal 'anda', CreditCard.brand?('6031998427187914') + end + + # Creditos directos a.k.a tarjeta d + def test_should_detect_tarjetad_card + assert_equal 'tarjeta-d', CreditCard.brand?('6018282227431033') + end + def test_should_detect_creditel_card assert_equal 'creditel', CreditCard.brand?('6019330047539016') end @@ -273,6 +361,9 @@ def test_should_detect_cabal_card assert_equal 'cabal', CreditCard.brand?('6044009000000000') assert_equal 'cabal', CreditCard.brand?('5896575500000000') assert_equal 'cabal', CreditCard.brand?('6035224400000000') + assert_equal 'cabal', CreditCard.brand?('6502723300000000') + assert_equal 'cabal', CreditCard.brand?('6500870000000000') + assert_equal 'cabal', CreditCard.brand?('6509000000000000') end def test_should_detect_unionpay_card @@ -412,6 +503,43 @@ def test_electron_cards assert_false electron_test.call('42496200000000000') end + def test_should_detect_panal_card + assert_equal 'panal', CreditCard.brand?('6020490000000000') + end + + def test_detecting_full_range_of_verve_card_numbers + verve = '506099000000000' + + assert_equal 15, verve.length + assert_not_equal 'verve', CreditCard.brand?(verve) + + 4.times do + verve << '0' + assert_equal 'verve', CreditCard.brand?(verve), "Failed for bin #{verve}" + end + + assert_equal 19, verve.length + + verve << '0' + assert_not_equal 'verve', CreditCard.brand?(verve) + end + + def test_should_detect_verve + credit_cards = %w[5060990000000000 + 506112100000000000 + 5061351000000000000 + 5061591000000000 + 506175100000000000 + 5078801000000000000 + 5079381000000000 + 637058100000000000 + 5079400000000000000 + 507879000000000000 + 5061930000000000 + 506136000000000000] + credit_cards.all? { |cc| CreditCard.brand?(cc) == 'verve' } + end + def test_credit_card? assert credit_card.credit_card? end diff --git a/test/unit/credit_card_test.rb b/test/unit/credit_card_test.rb index 23b2fa42743..595b3698bfa 100644 --- a/test/unit/credit_card_test.rb +++ b/test/unit/credit_card_test.rb @@ -5,6 +5,7 @@ def setup CreditCard.require_verification_value = false @visa = credit_card('4779139500118580', brand: 'visa') @maestro = credit_card('676700000000000000', brand: 'maestro', verification_value: '') + @bp_plus = credit_card('70501 501021600 378', brand: 'bp_plus') end def teardown @@ -173,7 +174,7 @@ def test_expired_card_should_have_one_error_on_year end def test_should_identify_wrong_card_brand - c = credit_card(brand: 'master') + c = credit_card('4779139500118580', brand: 'master') assert_not_valid c end @@ -438,4 +439,11 @@ def test_should_report_as_emv_if_icc_data_present def test_should_not_report_as_emv_if_icc_data_not_present refute CreditCard.new.emv? end + + def test_bp_plus_number_validation + assert_valid @bp_plus + assert_include @bp_plus.number, ' ' + assert_equal @bp_plus.brand, 'bp_plus' + assert @bp_plus.allow_spaces_in_card? + end end diff --git a/test/unit/fixtures_test.rb b/test/unit/fixtures_test.rb index b72720d928c..1b99051a5dd 100644 --- a/test/unit/fixtures_test.rb +++ b/test/unit/fixtures_test.rb @@ -2,7 +2,7 @@ class FixturesTest < Test::Unit::TestCase def test_sort - keys = YAML.safe_load(File.read(ActiveMerchant::Fixtures::DEFAULT_CREDENTIALS), [], [], true).keys + keys = YAML.safe_load(File.read(ActiveMerchant::Fixtures::DEFAULT_CREDENTIALS), aliases: true).keys assert_equal( keys, keys.sort diff --git a/test/unit/gateways/adyen_test.rb b/test/unit/gateways/adyen_test.rb index db7c7839ce6..e0355cfba68 100644 --- a/test/unit/gateways/adyen_test.rb +++ b/test/unit/gateways/adyen_test.rb @@ -12,52 +12,64 @@ def setup @bank_account = check() - @credit_card = credit_card('4111111111111111', + @credit_card = credit_card( + '4111111111111111', month: 8, year: 2018, first_name: 'Test', last_name: 'Card', verification_value: '737', - brand: 'visa') + brand: 'visa' + ) - @elo_credit_card = credit_card('5066 9911 1111 1118', + @elo_credit_card = credit_card( + '5066 9911 1111 1118', month: 10, year: 2020, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'elo') + brand: 'elo' + ) - @cabal_credit_card = credit_card('6035 2277 1642 7021', + @cabal_credit_card = credit_card( + '6035 2277 1642 7021', month: 10, year: 2020, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'cabal') + brand: 'cabal' + ) - @unionpay_credit_card = credit_card('8171 9999 0000 0000 021', + @unionpay_credit_card = credit_card( + '8171 9999 0000 0000 021', month: 10, year: 2030, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'unionpay') + brand: 'unionpay' + ) @three_ds_enrolled_card = credit_card('4212345678901237', brand: :visa) - @apple_pay_card = network_tokenization_credit_card('4111111111111111', + @apple_pay_card = network_tokenization_credit_card( + '4111111111111111', payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', month: '08', year: '2018', source: :apple_pay, - verification_value: nil) + verification_value: nil + ) - @nt_credit_card = network_tokenization_credit_card('4895370015293175', + @nt_credit_card = network_tokenization_credit_card( + '4895370015293175', brand: 'visa', eci: '07', source: :network_token, - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) @amount = 100 @@ -165,13 +177,14 @@ def test_failed_authorize_with_unexpected_3ds @gateway.expects(:ssl_post).returns(successful_authorize_with_3ds_response) response = @gateway.authorize(@amount, @three_ds_enrolled_card, @options) assert_failure response - assert_match 'Received unexpected 3DS authentication response', response.message + assert_match 'Received unexpected 3DS authentication response, but a 3DS initiation flag was not included in the request.', response.message end def test_successful_authorize_with_recurring_contract_type stub_comms do @gateway.authorize(100, @credit_card, @options.merge({ recurring_contract_type: 'ONECLICK' })) end.check_request do |_endpoint, data, _headers| + assert_equal 'john.smith@test.com', JSON.parse(data)['shopperEmail'] assert_equal 'ONECLICK', JSON.parse(data)['recurring']['contract'] end.respond_with(successful_authorize_response) end @@ -258,6 +271,54 @@ def test_unknown_error_code_mapping assert_equal '702', response.error_code end + def test_billing_address_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_billing_address_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:incorrect_address], response.error_code + end + + def test_cvc_length_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_cvc_validation_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:invalid_cvc], response.error_code + end + + def test_invalid_card_number_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_invalid_card_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:incorrect_number], response.error_code + end + + def test_invalid_amount_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_invalid_amount_response) + + response = @gateway.authorize(nil, @credit_card, @options) + assert_failure response + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:invalid_amount], response.error_code + end + + def test_invalid_access_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_not_allowed_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:config_error], response.error_code + end + + def test_unknown_reason_error_code_mapping + @gateway.expects(:ssl_post).returns(failed_unknown_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal AdyenGateway::STANDARD_ERROR_CODE[:processing_error], response.error_code + end + def test_failed_authorise3d @gateway.expects(:ssl_post).returns(failed_authorize_response) @@ -276,6 +337,33 @@ def test_failed_authorise3ds2 assert_failure response end + def test_failed_authorise_visa + @gateway.expects(:ssl_post).returns(failed_authorize_visa_response) + + response = @gateway.send(:commit, 'authorise', {}, {}) + + assert_equal 'Refused | 01: Refer to card issuer', response.message + assert_failure response + end + + def test_failed_authorise_mastercard + @gateway.expects(:ssl_post).returns(failed_authorize_mastercard_response) + + response = @gateway.send(:commit, 'authorise', {}, {}) + + assert_equal 'Refused | 01 : New account information available', response.message + assert_failure response + end + + def test_failed_authorise_mastercard_raw_error_message + @gateway.expects(:ssl_post).returns(failed_authorize_mastercard_response) + + response = @gateway.send(:commit, 'authorise', {}, { raw_error_message: true }) + + assert_equal 'Refused | 01: Refer to card issuer', response.message + assert_failure response + end + def test_successful_capture @gateway.expects(:ssl_post).returns(successful_capture_response) response = @gateway.capture(@amount, '7914775043909934') @@ -292,6 +380,14 @@ def test_failed_capture assert_failure response end + def test_successful_capture_with_shopper_statement + stub_comms do + @gateway.capture(@amount, '7914775043909934', @options.merge(shopper_statement: 'test1234')) + end.check_request do |_endpoint, data, _headers| + assert_equal 'test1234', JSON.parse(data)['additionalData']['shopperStatement'] + end.respond_with(successful_capture_response) + end + def test_successful_purchase_with_credit_card response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) @@ -614,6 +710,30 @@ def test_stored_credential_unscheduled_mit_used assert_success response end + def test_skip_mpi_data_field_omits_mpi_hash + options = { + billing_address: address(), + shipping_address: address(), + shopper_reference: 'John Smith', + order_id: '1001', + description: 'AM test', + currency: 'GBP', + customer: '123', + skip_mpi_data: 'Y', + shopper_interaction: 'ContAuth', + recurring_processing_model: 'Subscription', + network_transaction_id: '123ABC' + } + response = stub_comms do + @gateway.authorize(@amount, @apple_pay_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"shopperInteraction":"ContAuth"/, data) + assert_match(/"recurringProcessingModel":"Subscription"/, data) + refute_includes data, 'mpiData' + end.respond_with(successful_authorize_response) + assert_success response + end + def test_nonfractional_currency_handling stub_comms do @gateway.authorize(200, @credit_card, @options.merge(currency: 'JPY')) @@ -677,6 +797,29 @@ def test_successful_credit assert_success response end + def test_successful_payout_with_credit_card + payout_options = { + reference: 'P9999999999999999', + email: 'john.smith@test.com', + ip: '77.110.174.153', + shopper_reference: 'John Smith', + billing_address: @us_address, + nationality: 'NL', + order_id: 'P9999999999999999', + date_of_birth: '1990-01-01', + payout: true + } + + stub_comms do + @gateway.credit(2500, @credit_card, payout_options) + end.check_request do |endpoint, data, _headers| + assert_match(/payout/, endpoint) + assert_match(/"dateOfBirth\":\"1990-01-01\"/, data) + assert_match(/"nationality\":\"NL\"/, data) + assert_match(/"shopperName\":{\"firstName\":\"Test\",\"lastName\":\"Card\"}/, data) + end.respond_with(successful_payout_response) + end + def test_successful_void @gateway.expects(:ssl_post).returns(successful_void_response) response = @gateway.void('7914775043909934') @@ -737,6 +880,14 @@ def test_successful_tokenize_only_store assert_equal '#8835205392522157#', response.authorization end + def test_successful_tokenize_only_store_with_ntid + stub_comms do + @gateway.store(@credit_card, @options.merge({ tokenize_only: true, network_transaction_id: '858435661128555' })) + end.check_request do |_endpoint, data, _headers| + assert_equal '858435661128555', JSON.parse(data)['additionalData']['networkTxReference'] + end.respond_with(successful_store_response) + end + def test_successful_store response = stub_comms do @gateway.store(@credit_card, @options) @@ -873,14 +1024,16 @@ def test_scrub_network_tokenization_card def test_shopper_data post = { card: { billingAddress: {} } } - @gateway.send(:add_shopper_data, post, @options) + @gateway.send(:add_shopper_data, post, @credit_card, @options) + @gateway.send(:add_extra_data, post, @credit_card, @options) assert_equal 'john.smith@test.com', post[:shopperEmail] assert_equal '77.110.174.153', post[:shopperIP] end def test_shopper_data_backwards_compatibility post = { card: { billingAddress: {} } } - @gateway.send(:add_shopper_data, post, @options_shopper_data) + @gateway.send(:add_shopper_data, post, @credit_card, @options_shopper_data) + @gateway.send(:add_extra_data, post, @credit_card, @options_shopper_data) assert_equal 'john2.smith@test.com', post[:shopperEmail] assert_equal '192.168.100.100', post[:shopperIP] end @@ -908,6 +1061,16 @@ def test_add_address assert_equal @options[:shipping_address][:country], post[:deliveryAddress][:country] end + def test_address_override_that_will_swap_housenumberorname_and_street + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(address_override: true)) + end.check_request do |_endpoint, data, _headers| + assert_match(/"houseNumberOrName":"456 My Street"/, data) + assert_match(/"street":"Apt 1"/, data) + end.respond_with(successful_authorize_response) + assert_success response + end + def test_successful_auth_phone options = @options.merge(billing_address: { phone: 1234567890 }) response = stub_comms do @@ -1152,6 +1315,171 @@ def test_authorize_with_sub_sellers assert_success response end + def test_level_2_data + level_2_options = { + total_tax_amount: '160', + customer_reference: '101' + } + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(level_2_data: level_2_options)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + additional_data = parsed['additionalData'] + assert_equal additional_data['enhancedSchemeData.totalTaxAmount'], level_2_options[:total_tax_amount] + assert_equal additional_data['enhancedSchemeData.customerReference'], level_2_options[:customer_reference] + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_level_3_data + level_3_options = { + total_tax_amount: '12800', + customer_reference: '101', + freight_amount: '300', + destination_state_province_code: 'NYC', + ship_from_postal_code: '1082GM', + order_date: '101216', + destination_postal_code: '1082GM', + destination_country_code: 'NLD', + duty_amount: '500', + items: [ + { + description: 'T16 Test products 1', + product_code: 'TEST120', + commodity_code: 'COMMCODE1', + quantity: '5', + unit_of_measure: 'm', + unit_price: '1000', + discount_amount: '60', + total_amount: '4940' + } + ] + } + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(level_3_data: level_3_options)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + additional_data = parsed['additionalData'] + leve_3_keys = ['enhancedSchemeData.freightAmount', 'enhancedSchemeData.destinationStateProvinceCode', + 'enhancedSchemeData.shipFromPostalCode', 'enhancedSchemeData.orderDate', 'enhancedSchemeData.destinationPostalCode', + 'enhancedSchemeData.destinationCountryCode', 'enhancedSchemeData.dutyAmount', + 'enhancedSchemeData.itemDetailLine1.description', 'enhancedSchemeData.itemDetailLine1.productCode', + 'enhancedSchemeData.itemDetailLine1.commodityCode', 'enhancedSchemeData.itemDetailLine1.quantity', + 'enhancedSchemeData.itemDetailLine1.unitOfMeasure', 'enhancedSchemeData.itemDetailLine1.unitPrice', + 'enhancedSchemeData.itemDetailLine1.discountAmount', 'enhancedSchemeData.itemDetailLine1.totalAmount'] + + additional_data_keys = additional_data.keys + assert_all(leve_3_keys) { |item| additional_data_keys.include?(item) } + + mapper = { "enhancedSchemeData.freightAmount": 'freight_amount', + "enhancedSchemeData.destinationStateProvinceCode": 'destination_state_province_code', + "enhancedSchemeData.shipFromPostalCode": 'ship_from_postal_code', + "enhancedSchemeData.orderDate": 'order_date', + "enhancedSchemeData.destinationPostalCode": 'destination_postal_code', + "enhancedSchemeData.destinationCountryCode": 'destination_country_code', + "enhancedSchemeData.dutyAmount": 'duty_amount' } + + mapper.each do |item| + assert_equal additional_data[item[0]], level_3_options[item[1]] + end + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_succesful_additional_airline_data + airline_data = { + agency_invoice_number: 'BAC123', + agency_plan_name: 'plan name', + airline_code: '434234', + airline_designator_code: '1234', + boarding_fee: '100', + computerized_reservation_system: 'abcd', + customer_reference_number: 'asdf1234', + document_type: 'cc', + leg: { + carrier_code: 'KL' + }, + passenger: { + first_name: 'Joe', + last_name: 'Doe' + } + } + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(additional_data_airline: airline_data)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + additional_data = parsed['additionalData'] + assert_equal additional_data['airline.agency_invoice_number'], airline_data[:agency_invoice_number] + assert_equal additional_data['airline.agency_plan_name'], airline_data[:agency_plan_name] + assert_equal additional_data['airline.airline_code'], airline_data[:airline_code] + assert_equal additional_data['airline.airline_designator_code'], airline_data[:airline_designator_code] + assert_equal additional_data['airline.boarding_fee'], airline_data[:boarding_fee] + assert_equal additional_data['airline.computerized_reservation_system'], airline_data[:computerized_reservation_system] + assert_equal additional_data['airline.customer_reference_number'], airline_data[:customer_reference_number] + assert_equal additional_data['airline.document_type'], airline_data[:document_type] + assert_equal additional_data['airline.flight_date'], airline_data[:flight_date] + assert_equal additional_data['airline.ticket_issue_address'], airline_data[:abcqwer] + assert_equal additional_data['airline.ticket_number'], airline_data[:ticket_number] + assert_equal additional_data['airline.travel_agency_code'], airline_data[:travel_agency_code] + assert_equal additional_data['airline.travel_agency_name'], airline_data[:travel_agency_name] + assert_equal additional_data['airline.passenger_name'], airline_data[:passenger_name] + assert_equal additional_data['airline.leg.carrier_code'], airline_data[:leg][:carrier_code] + assert_equal additional_data['airline.leg.class_of_travel'], airline_data[:leg][:class_of_travel] + assert_equal additional_data['airline.passenger.first_name'], airline_data[:passenger][:first_name] + assert_equal additional_data['airline.passenger.last_name'], airline_data[:passenger][:last_name] + assert_equal additional_data['airline.passenger.telephone_number'], airline_data[:passenger][:telephone_number] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_additional_data_lodging + lodging_data = { + check_in_date: '20230822', + check_out_date: '20230830', + customer_service_toll_free_number: '234234', + fire_safety_act_indicator: 'abc123', + folio_cash_advances: '1234667', + folio_number: '32343', + food_beverage_charges: '1234', + no_show_indicator: 'Y', + prepaid_expenses: '100', + property_phone_number: '54545454', + number_of_nights: '5' + } + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(additional_data_lodging: lodging_data)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + additional_data = parsed['additionalData'] + assert_equal additional_data['lodging.checkInDate'], lodging_data[:check_in_date] + assert_equal additional_data['lodging.checkOutDate'], lodging_data[:check_out_date] + assert_equal additional_data['lodging.customerServiceTollFreeNumber'], lodging_data[:customer_service_toll_free_number] + assert_equal additional_data['lodging.fireSafetyActIndicator'], lodging_data[:fire_safety_act_indicator] + assert_equal additional_data['lodging.folioCashAdvances'], lodging_data[:folio_cash_advances] + assert_equal additional_data['lodging.folioNumber'], lodging_data[:folio_number] + assert_equal additional_data['lodging.foodBeverageCharges'], lodging_data[:food_beverage_charges] + assert_equal additional_data['lodging.noShowIndicator'], lodging_data[:no_show_indicator] + assert_equal additional_data['lodging.prepaidExpenses'], lodging_data[:prepaid_expenses] + assert_equal additional_data['lodging.propertyPhoneNumber'], lodging_data[:property_phone_number] + assert_equal additional_data['lodging.room1.numberOfNights'], lodging_data[:number_of_nights] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_additional_extra_data + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(store: 'test store')) + end.check_request do |_endpoint, data, _headers| + assert_equal JSON.parse(data)['store'], 'test store' + end.respond_with(successful_authorize_response) + assert_success response + end + def test_extended_avs_response response = stub_comms do @gateway.verify(@credit_card, @options) @@ -1493,6 +1821,35 @@ def failed_authorize_3ds2_response RESPONSE end + def failed_authorize_visa_response + <<-RESPONSE + { + "additionalData": + { + "refusalReasonRaw": "01: Refer to card issuer" + }, + "refusalReason": "Refused", + "pspReference":"8514775559925128", + "resultCode":"Refused" + } + RESPONSE + end + + def failed_authorize_mastercard_response + <<-RESPONSE + { + "additionalData": + { + "refusalReasonRaw": "01: Refer to card issuer", + "merchantAdviceCode": "01 : New account information available" + }, + "refusalReason": "Refused", + "pspReference":"8514775559925128", + "resultCode":"Refused" + } + RESPONSE + end + def successful_capture_response <<-RESPONSE { @@ -1542,6 +1899,31 @@ def successful_credit_response RESPONSE end + def successful_payout_response + <<-RESPONSE + { + "additionalData": + { + "liabilityShift": "false", + "authCode": "081439", + "avsResult": "0 Unknown", + "retry.attempt1.acquirerAccount": "TestPmmAcquirerAccount", + "threeDOffered": "false", + "retry.attempt1.acquirer": "TestPmmAcquirer", + "authorisationMid": "50", + "acquirerAccountCode": "TestPmmAcquirerAccount", + "cvcResult": "0 Unknown", + "retry.attempt1.responseCode": "Approved", + "threeDAuthenticated": "false", + "retry.attempt1.rawResponse": "AUTHORISED" + }, + "pspReference": "GMTN2VTQGJHKGK82", + "resultCode": "Authorised", + "authCode": "081439" + } + RESPONSE + end + def failed_credit_response <<-RESPONSE { @@ -1624,6 +2006,72 @@ def successful_verify_response RESPONSE end + def failed_unknown_response + <<~RESPONSE + { + "status": 422, + "errorCode": "0", + "message": "An unknown error occurred", + "errorType": "validation" + } + RESPONSE + end + + def failed_not_allowed_response + <<~RESPONSE + { + "status": 422, + "errorCode": "10", + "message": "You are not allowed to perform this action", + "errorType": "validation" + } + RESPONSE + end + + def failed_invalid_amount_response + <<~RESPONSE + { + "status": 422, + "errorCode": "100", + "message": "There is no amount specified in the request", + "errorType": "validation" + } + RESPONSE + end + + def failed_invalid_card_response + <<~RESPONSE + { + "status": 422, + "errorCode": "101", + "message": "The specified card number is not valid", + "errorType": "validation" + } + RESPONSE + end + + def failed_cvc_validation_response + <<~RESPONSE + { + "status": 422, + "errorCode": "103", + "message": "The length of the CVC code is not correct for the given card number", + "errorType": "validation" + } + RESPONSE + end + + def failed_billing_address_response + <<~RESPONSE + { + "status": 422, + "errorCode": "104", + "message": "There was an error in the specified billing address fields", + "errorType": "validation" + } + RESPONSE + end + def failed_billing_field_response <<~RESPONSE { diff --git a/test/unit/gateways/airwallex_test.rb b/test/unit/gateways/airwallex_test.rb index 956a26bf310..ade541ce88e 100644 --- a/test/unit/gateways/airwallex_test.rb +++ b/test/unit/gateways/airwallex_test.rb @@ -21,8 +21,7 @@ def setup @declined_amount = 8014 @options = { - billing_address: address, - return_url: 'https://example.com' + billing_address: address } @stored_credential_cit_options = { initial_transaction: true, initiator: 'cardholder', reason_type: 'recurring', network_transaction_id: nil } @@ -52,12 +51,6 @@ def test_failed_purchase_with_declined_card assert_equal 'The card issuer declined this transaction. Please refer to the original response code.', response.message end - def test_purchase_without_return_url_raises_error - assert_raise ArgumentError do - @gateway.purchase(@amount, @credit_card, {}) - end - end - def test_successful_authorize @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response) @@ -77,10 +70,18 @@ def test_failed_authorize_with_declined_card assert_equal 'The card issuer declined this transaction. Please refer to the original response code.', response.message end - def test_authorize_without_return_url_raises_error - assert_raise ArgumentError do - @gateway.authorize(@amount, @credit_card, { auto_capture: false }) - end + def test_successful_authorize_with_return_url + return_url = 'https://example.com' + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(return_url: return_url)) + end.check_request do |endpoint, data, _headers| + assert_match(/\"return_url\":\"https:\/\/example.com\"/, data) unless endpoint == setup_endpoint + end.respond_with(successful_authorize_response) + + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message end def test_successful_authorize_with_3ds_v1_options @@ -156,6 +157,22 @@ def test_successful_purchase_with_3ds_version_formatting assert_equal 'AUTHORIZED', response.message end + def test_successful_skip_3ds_in_payment_intent + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ skip_3ds: true })) + end.check_request do |endpoint, data, _headers| + data = JSON.parse(data) + assert_match(data['payment_method_options']['card']['risk_control']['three_ds_action'], 'SKIP_3DS') if endpoint == setup_endpoint + end.respond_with(successful_purchase_response) + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ skip_3ds: 'true' })) + end.check_request do |endpoint, data, _headers| + data = JSON.parse(data) + assert_match(data['payment_method_options']['card']['risk_control']['three_ds_action'], 'SKIP_3DS') if endpoint == setup_endpoint + end.respond_with(successful_purchase_response) + end + def test_successful_capture @gateway.expects(:ssl_post).returns(successful_capture_response) @@ -251,7 +268,7 @@ def test_refund_passes_both_ids end def test_purchase_passes_appropriate_request_id_per_call - request_id = "request_#{(Time.now.to_f.round(2) * 100).to_i}" + request_id = SecureRandom.uuid stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(request_id: request_id)) end.check_request do |_endpoint, data, _headers| @@ -265,18 +282,12 @@ def test_purchase_passes_appropriate_request_id_per_call end.respond_with(successful_purchase_response) end - def test_purchase_passes_appropriate_merchant_order_id_per_call - merchant_order_id = "order_#{(Time.now.to_f.round(2) * 100).to_i}" + def test_purchase_passes_appropriate_merchant_order_id + merchant_order_id = SecureRandom.uuid stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(merchant_order_id: merchant_order_id)) end.check_request do |_endpoint, data, _headers| - if data.include?('payment_method') - # check for this on the purchase call - assert_match(/\"merchant_order_id\":\"#{merchant_order_id}\"/, data) - else - # check for this on the create_payment_intent calls - assert_match(/\"merchant_order_id\":\"#{merchant_order_id}_setup\"/, data) - end + assert_match(/\"merchant_order_id\":\"#{merchant_order_id}\"/, data) end.respond_with(successful_purchase_response) end @@ -289,6 +300,15 @@ def test_purchase_passes_currency_code end.respond_with(successful_purchase_response) end + def test_purchase_passes_referrer_data + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + # only look for referrer data on the create_payment_intent request + assert_match(/\"referrer_data\":{\"type\":\"spreedly\"}/, data) if data.include?('_setup') + end.respond_with(successful_purchase_response) + end + def test_purchase_passes_descriptor stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(description: 'a simple test')) diff --git a/test/unit/gateways/alelo_test.rb b/test/unit/gateways/alelo_test.rb new file mode 100644 index 00000000000..3e6c9f0c1c9 --- /dev/null +++ b/test/unit/gateways/alelo_test.rb @@ -0,0 +1,371 @@ +require 'test_helper' + +class AleloTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = AleloGateway.new(fixtures(:alelo)) + @credit_card = credit_card + @amount = 100 + + @options = { + order_id: 'f63b625e-331e-490a-b15c-50b4087ca64f', + establishment_code: '000002007690360', + sub_merchant_mcc: '5499', + player_identification: '1', + description: 'Store Purchase', + external_trace_number: '123456', + uuid: '49bc3a5c-2e0f-11ed-a261-0242ac120002' + } + end + + def test_required_client_id_and_client_secret + error = assert_raises ArgumentError do + AleloGateway.new + end + + assert_equal 'Missing required parameter: client_id', error.message + end + + def test_supported_card_types + assert_equal AleloGateway.supported_cardtypes, %i[visa master american_express discover] + end + + def test_supported_countries + assert_equal AleloGateway.supported_countries, ['BR'] + end + + def test_support_scrubbing_flag_enabled + assert @gateway.supports_scrubbing? + end + + def test_sucessful_fetch_access_token_with_proper_client_id_client_secret + @gateway = AleloGateway.new(client_id: 'abc123', client_secret: 'def456') + access_token_expectation! @gateway + + resp = @gateway.send(:fetch_access_token) + assert_kind_of Response, resp + assert_equal 'abc123', resp.message + end + + def test_successful_remote_encryption_key + @gateway = AleloGateway.new(client_id: 'abc123', client_secret: 'def456') + encryption_key_expectation! @gateway + + resp = @gateway.send(:remote_encryption_key, 'abc123') + + assert_kind_of Response, resp + assert_equal 'def456', resp.message + assert_equal 'some-uuid', resp.params['uuid'] + end + + def test_successful_purchase_with_provided_credentials + key, secret_key = test_key true + + @gateway.options[:encryption_key] = key + @gateway.options[:access_token] = 'abc123' + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + decrypted = JOSE::JWE.block_decrypt(secret_key, JSON.parse(data)['token']).first + request = JSON.parse(decrypted, symbolize_names: true) + + assert_equal @options[:order_id], request[:requestId] + assert_equal 1.0, request[:amount] + assert_equal @credit_card.number, request[:cardNumber] + assert_equal @credit_card.name, request[:cardholderName] + assert_equal @credit_card.month, request[:expirationMonth] + assert_equal @credit_card.year - 2000, request[:expirationYear] + assert_equal '3', request[:captureType] + assert_equal @credit_card.verification_value, request[:securityCode] + assert_equal @options[:establishment_code], request[:establishmentCode] + assert_equal @options[:player_identification], request[:playerIdentification] + assert_equal @options[:sub_merchant_mcc], request[:subMerchantCode] + assert_equal @options[:external_trace_number], request[:externalTraceNumber] + end.respond_with(successful_capture_response) + + assert_success response + assert_equal 'f63b625e-331e-490a-b15c-50b4087ca64f', response.authorization + + # Check new values for credentials + assert_nil response.params['access_token'] + assert_nil response.params['encryption_key'] + assert_nil response.params['encryption_uuid'] + end + + def test_successful_purchase_with_no_provided_credentials + key = test_key + @gateway.expects(:ssl_post).times(2).returns({ access_token: 'abc123' }.to_json, successful_capture_response) + @gateway.expects(:ssl_get).returns({ publicKey: key, uuid: 'some-uuid' }.to_json) + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_kind_of MultiResponse, response + assert_equal 3, response.responses.size + assert_equal 'abc123', response.responses.first.message + assert_equal key, response.responses[1].message + + # Check new values for credentials + assert_equal 'abc123', response.params['access_token'] + assert_equal key, response.params['encryption_key'] + assert_equal 'some-uuid', response.params['encryption_uuid'] + end + + def test_sucessful_retry_with_expired_encryption_key + key = test_key + @gateway.options[:encryption_key] = key + @gateway.options[:access_token] = 'abc123' + + # Expectations + # ssl_post purchace => raises a 401 + # ssl_get => key + # ssl_post => Final purchase success + @gateway.expects(:ssl_post). + times(2). + raises(ActiveMerchant::ResponseError.new(stub('401 Response', code: '401'))). + then.returns(successful_capture_response) + + @gateway.expects(:ssl_get).returns({ publicKey: key, uuid: 'some-uuid' }.to_json) + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_kind_of MultiResponse, response + assert_equal 2, response.responses.size + assert_equal key, response.responses.first.message + end + + def test_sucessful_retry_with_expired_access_token_and_encryption_key + key = test_key + @gateway.options[:encryption_key] = key + @gateway.options[:access_token] = 'abc123' + + # Expectations + # ssl_post purchace => raises a 401 + # ssl_get get key => raise a 401 + # ssl_post => access_token + # ssl_get => key + # ssl_post => Final purchase success + @gateway.expects(:ssl_post). + times(3). + raises(ActiveMerchant::ResponseError.new(stub('401 Response', code: '401'))). + then.returns({ access_token: 'abc123' }.to_json, successful_capture_response) + + @gateway.expects(:ssl_get). + times(2). + raises(ActiveMerchant::ResponseError.new(stub('401 Response', code: '401'))). + then.returns({ publicKey: key, uuid: 'some-uuid' }.to_json) + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_kind_of MultiResponse, response + assert_equal 3, response.responses.size + assert_equal 'abc123', response.responses.first.message + assert_equal key, response.responses[1].message + end + + def test_sucessful_retry_with_missing_uuid_404 + key = test_key + @gateway.options[:encryption_key] = key + @gateway.options[:access_token] = 'abc123' + + # Expectations + # ssl_post purchace => raises a 401 + # ssl_get get key => raise a 401 + # ssl_post => access_token + # ssl_get => key + # ssl_post => Final purchase success + @gateway.expects(:ssl_post). + times(3). + raises(ActiveMerchant::ResponseError.new(stub('404 Response', code: '404'))). + then.returns({ access_token: 'abc123' }.to_json, successful_capture_response) + + @gateway.expects(:ssl_get). + times(2). + raises(ActiveMerchant::ResponseError.new(stub('404 Response', code: '404'))). + then.returns({ publicKey: key, uuid: 'some-uuid' }.to_json) + + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_kind_of MultiResponse, response + assert_equal 3, response.responses.size + assert_equal 'abc123', response.responses.first.message + assert_equal key, response.responses[1].message + end + + def test_detecting_successfull_response_from_capture + assert @gateway.send :success_from, 'capture/transaction', { status: 'CONFIRMADA' } + end + + def test_detecting_successfull_response_from_refund + assert @gateway.send :success_from, 'capture/transaction/refund', { status: 'ESTORNADA' } + end + + def test_get_response_message_from_messages_key + message = @gateway.send :message_from, { messages: 'hello', messageUser: 'world' } + assert_equal 'hello', message + end + + def test_get_response_message_from_message_user + message = @gateway.send :message_from, { messages: nil, messageUser: 'world' } + assert_equal 'world', message + end + + def test_url_generation_from_action + action = 'test' + assert_equal @gateway.test_url + action, @gateway.send(:url, action) + end + + def test_request_headers_building + gateway = AleloGateway.new(client_id: 'abc123', client_secret: 'def456') + headers = gateway.send :request_headers, 'access_123' + + assert_equal 'application/json', headers['Accept'] + assert_equal 'abc123', headers['X-IBM-Client-Id'] + assert_equal 'def456', headers['X-IBM-Client-Secret'] + assert_equal 'Bearer access_123', headers['Authorization'] + end + + def test_scrub + assert @gateway.supports_scrubbing? + + pre_scrubbed = File.read('test/unit/transcripts/alelo_purchase') + post_scrubbed = File.read('test/unit/transcripts/alelo_purchase_scrubbed') + + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_success_payload_encryption + @gateway.options[:access_token] = 'abc123' + @gateway.options[:encryption_key] = test_key + @gateway.options[:encryption_uuid] = SecureRandom.uuid + + credentials = @gateway.send(:ensure_credentials) + jwe, = @gateway.send(:encrypt_payload, { hello: 'world' }, credentials, {}) + + refute_nil JSON.parse(jwe)['token'] + refute_nil JSON.parse(jwe)['uuid'] + end + + def test_ensure_encryption_format + key, secret_key = test_key true + body = { hello: 'world' } + @gateway.options[:access_token] = 'abc123' + @gateway.options[:encryption_key] = key + credentials = @gateway.send(:ensure_credentials) + + jwe, _cred = @gateway.send(:encrypt_payload, body, credentials, {}) + parsed_jwe = JSON.parse(jwe, symbolize_names: true) + refute_nil parsed_jwe[:token] + + decrypted = JOSE::JWE.block_decrypt(secret_key, parsed_jwe[:token]).first + assert_equal body.to_json, decrypted + end + + def test_ensure_credentials_with_provided_access_token_and_key + @gateway.options[:access_token] = 'abc123' + @gateway.options[:encryption_key] = 'def456' + + credentials = @gateway.send :ensure_credentials + + assert_equal @gateway.options[:access_token], credentials[:access_token] + assert_equal @gateway.options[:encryption_key], credentials[:key] + assert_nil credentials[:multiresp] + end + + def test_ensure_credentials_with_access_token_and_not_key + encryption_key_expectation! @gateway + + @gateway.options[:access_token] = 'abc123' + credentials = @gateway.send :ensure_credentials + + assert_equal @gateway.options[:access_token], credentials[:access_token] + assert_equal 'def456', credentials[:key] + refute_nil credentials[:multiresp] + assert_equal 1, credentials[:multiresp].responses.size + end + + def test_ensure_credentials_with_key_but_not_access_token + @gateway = AleloGateway.new(client_id: 'abc123', client_secret: 'def456') + @gateway.options[:encryption_key] = 'xx_no_key_xx' + + access_token_expectation! @gateway + encryption_key_expectation! @gateway + + credentials = @gateway.send :ensure_credentials + + assert_equal 'abc123', credentials[:access_token] + assert_equal 'def456', credentials[:key] + refute_nil credentials[:multiresp] + assert_equal 2, credentials[:multiresp].responses.size + end + + def test_credit_card_year_should_be_an_integer + post = {} + @gateway.send :add_payment, post, @credit_card + + assert_kind_of Integer, post[:expirationYear] + assert_equal 2, post[:expirationYear].digits.size + end + + private + + def test_key(with_sk = false) + jwk_rsa_sk = JOSE::JWK.generate_key([:rsa, 4096]) + jwk_rsa_pk = JOSE::JWK.to_public(jwk_rsa_sk) + + pem = jwk_rsa_pk.to_pem.split("\n") + pem.pop + pem.shift + + return pem.join unless with_sk + + return pem.join, jwk_rsa_sk + end + + def access_token_expectation!(gateway, access_token = 'abc123') + url = "#{@gateway.class.test_url}captura-oauth-provider/oauth/token" + params = [ + 'grant_type=client_credentials', + 'client_id=abc123', + 'client_secret=def456', + 'scope=%2Fcapture' + ].join('&') + + headers = { + 'Accept' => 'application/json', + 'Content-Type' => 'application/x-www-form-urlencoded' + } + + gateway.expects(:ssl_post).with(url, params, headers).returns({ access_token: access_token }.to_json) + end + + def encryption_key_expectation!(gateway, public_key = 'def456') + url = "#{@gateway.class.test_url}capture/key" + headers = { + 'Accept' => 'application/json', + 'X-IBM-Client-Id' => gateway.options[:client_id], + 'X-IBM-Client-Secret' => gateway.options[:client_secret], + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer abc123' + } + + @gateway.expects(:ssl_get).with(url, headers).returns({ publicKey: public_key, uuid: 'some-uuid' }.to_json) + end + + def successful_capture_response + { + requestId: 'f63b625e-331e-490a-b15c-50b4087ca64f', + dateTime: '211105181958', + returnCode: '00', + nsu: '00123', + amount: '0.10', + maskedCard: '506758******7013', + authorizationCode: '735977', + messages: 'Transação Confirmada com sucesso.', + status: 'CONFIRMADA', + playerIdentification: '4', + captureType: '3' + }.to_json + end +end diff --git a/test/unit/gateways/authorize_net_arb_test.rb b/test/unit/gateways/authorize_net_arb_test.rb index 2b32c503d60..dfcc2d4c9ae 100644 --- a/test/unit/gateways/authorize_net_arb_test.rb +++ b/test/unit/gateways/authorize_net_arb_test.rb @@ -18,7 +18,9 @@ def setup def test_successful_recurring @gateway.expects(:ssl_post).returns(successful_recurring_response) - response = @gateway.recurring(@amount, @credit_card, + response = @gateway.recurring( + @amount, + @credit_card, billing_address: address.merge(first_name: 'Jim', last_name: 'Smith'), interval: { length: 10, @@ -27,7 +29,8 @@ def test_successful_recurring duration: { start_date: Time.now.strftime('%Y-%m-%d'), occurrences: 30 - }) + } + ) assert_instance_of Response, response assert response.success? diff --git a/test/unit/gateways/authorize_net_test.rb b/test/unit/gateways/authorize_net_test.rb index 4e6a8ed3988..b0f3b957b0e 100644 --- a/test/unit/gateways/authorize_net_test.rb +++ b/test/unit/gateways/authorize_net_test.rb @@ -789,7 +789,10 @@ def test_failed_void_using_stored_card def test_successful_verify response = stub_comms do - @gateway.verify(@credit_card) + @gateway.verify(@credit_card, @options) + end.check_request do |_endpoint, data, _headers| + doc = parse(data) + assert_equal '1.00', doc.at_xpath('//transactionRequest/amount').content if doc.at_xpath('//transactionRequest/transactionType').content == 'authOnlyTransaction' end.respond_with(successful_authorize_response, successful_void_response) assert_success response end @@ -802,6 +805,20 @@ def test_successful_verify_failed_void assert_match %r{This transaction has been approved}, response.message end + def test_successful_verify_with_0_auth_card + options = { + verify_amount: 0, + billing_address: { + address1: '123 St', + zip: '88888' + } + } + response = stub_comms do + @gateway.verify(@credit_card, options) + end.respond_with(successful_authorize_response) + assert_success response + end + def test_unsuccessful_verify response = stub_comms do @gateway.verify(@credit_card, @options) @@ -923,6 +940,20 @@ def test_address end.respond_with(successful_authorize_response) end + def test_address_with_alternate_phone_number_field + stub_comms do + @gateway.authorize(@amount, @credit_card, billing_address: { address1: '164 Waverley Street', country: 'US', state: 'CO', phone_number: '(555)555-5555', fax: '(555)555-4444' }) + end.check_request do |_endpoint, data, _headers| + parse(data) do |doc| + assert_equal 'CO', doc.at_xpath('//billTo/state').content, data + assert_equal '164 Waverley Street', doc.at_xpath('//billTo/address').content, data + assert_equal 'US', doc.at_xpath('//billTo/country').content, data + assert_equal '(555)555-5555', doc.at_xpath('//billTo/phoneNumber').content + assert_equal '(555)555-4444', doc.at_xpath('//billTo/faxNumber').content + end + end.respond_with(successful_authorize_response) + end + def test_address_with_empty_billing_address stub_comms do @gateway.authorize(@amount, @credit_card) @@ -1097,6 +1128,20 @@ def test_successful_bank_refund assert_success response end + def test_successful_bank_refund_truncates_long_name + response = stub_comms do + @gateway.refund(50, '12345667', account_type: 'checking', routing_number: '123450987', account_number: '12345667', first_name: 'Louise', last_name: 'Belcher-Williamson') + end.check_request do |_endpoint, data, _headers| + parse(data) do |doc| + assert_equal 'checking', doc.at_xpath('//transactionRequest/payment/bankAccount/accountType').content + assert_equal '123450987', doc.at_xpath('//transactionRequest/payment/bankAccount/routingNumber').content + assert_equal '12345667', doc.at_xpath('//transactionRequest/payment/bankAccount/accountNumber').content + assert_equal 'Louise Belcher-William', doc.at_xpath('//transactionRequest/payment/bankAccount/nameOnAccount').content + end + end.respond_with(successful_refund_response) + assert_success response + end + def test_refund_passing_extra_info response = stub_comms do @gateway.refund(50, '123456789', card_number: @credit_card.number, first_name: 'Bob', last_name: 'Smith', zip: '12345', order_id: '1', description: 'Refund for order 1') @@ -1273,9 +1318,7 @@ def test_dont_include_cust_id_for_phone_numbers end def test_includes_shipping_name_when_different_from_billing_name - card = credit_card('4242424242424242', - first_name: 'billing', - last_name: 'name') + card = credit_card('4242424242424242', first_name: 'billing', last_name: 'name') options = { order_id: 'a' * 21, @@ -1296,9 +1339,7 @@ def test_includes_shipping_name_when_different_from_billing_name end def test_includes_shipping_name_when_passed_as_options - card = credit_card('4242424242424242', - first_name: 'billing', - last_name: 'name') + card = credit_card('4242424242424242', first_name: 'billing', last_name: 'name') shipping_address = address(first_name: 'shipping', last_name: 'lastname') shipping_address.delete(:name) @@ -1321,9 +1362,7 @@ def test_includes_shipping_name_when_passed_as_options end def test_truncation - card = credit_card('4242424242424242', - first_name: 'a' * 51, - last_name: 'a' * 51) + card = credit_card('4242424242424242', first_name: 'a' * 51, last_name: 'a' * 51) options = { order_id: 'a' * 21, @@ -1395,8 +1434,7 @@ def test_supports_scrubbing? end def test_successful_apple_pay_authorization_with_network_tokenization - credit_card = network_tokenization_credit_card('4242424242424242', - payment_cryptogram: '111111111100cryptogram') + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: '111111111100cryptogram') response = stub_comms do @gateway.authorize(@amount, credit_card) @@ -1414,8 +1452,7 @@ def test_successful_apple_pay_authorization_with_network_tokenization end def test_failed_apple_pay_authorization_with_network_tokenization_not_supported - credit_card = network_tokenization_credit_card('4242424242424242', - payment_cryptogram: '111111111100cryptogram') + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: '111111111100cryptogram') response = stub_comms do @gateway.authorize(@amount, credit_card) @@ -1469,7 +1506,85 @@ def test_verify_bad_credentials assert !@gateway.verify_credentials end - private + def test_0_amount_verify_with_no_zip + @options[:verify_amount] = 0 + @options[:billing_address] = { zip: nil, address1: 'XYZ' } + + response = @gateway.verify(@credit_card, @options) + + assert_failure response + assert_equal 'Billing address including zip code is required for a 0 amount verify', response.message + end + + def test_verify_transcript_with_0_auth + stub_comms do + @options[:verify_amount] = 0 + @gateway.verify(@credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + doc = parse(data) + assert_equal '0.00', doc.at_xpath('//transactionRequest/amount').content if doc.at_xpath('//transactionRequest/transactionType').content == 'authOnlyTransaction' + end + end + + def test_verify_amount_with_bad_string + error = assert_raises(ArgumentError) do + @gateway.send :amount_for_verify, verify_amount: 'dog' + end + + assert_equal 'verify_amount value must be an integer', error.message + end + + def test_verify_amount_with_boolean + error = assert_raises(ArgumentError) do + @gateway.send :amount_for_verify, verify_amount: true + end + + assert_equal 'verify_amount value must be an integer', error.message + end + + def test_verify_amount_with_decimal + error = assert_raises(ArgumentError) do + @gateway.send :amount_for_verify, verify_amount: 0.125 + end + + assert_equal 'verify_amount value must be an integer', error.message + end + + def test_verify_amount_with_negative + error = assert_raises(ArgumentError) do + @gateway.send :amount_for_verify, verify_amount: -100 + end + + assert_equal 'verify_amount value must be an integer', error.message + end + + def test_verify_amount_with_string_as_number + assert_equal 200, @gateway.send(:amount_for_verify, verify_amount: '200') + end + + def test_verify_amount_with_zero_without_zip + error = assert_raises(ArgumentError) do + @gateway.send :amount_for_verify, verify_amount: 0, billing_address: { address1: 'street' } + end + + assert_equal 'Billing address including zip code is required for a 0 amount verify', error.message + end + + def test_verify_amount_with_zero_without_address + error = assert_raises(ArgumentError) do + @gateway.send :amount_for_verify, verify_amount: 0, billing_address: { zip: '051052' } + end + + assert_equal 'Billing address including zip code is required for a 0 amount verify', error.message + end + + def test_verify_amount_without_gsf + assert_equal 100, @gateway.send(:amount_for_verify, {}) + end + + def test_verify_amount_with_nil_value + assert_equal 100, @gateway.send(:amount_for_verify, { verify_amount: nil }) + end def pre_scrubbed <<-PRE_SCRUBBED @@ -2666,4 +2781,46 @@ def failed_refund_for_unsettled_payment_response XML end + + def unsuccessful_authorize_response_for_jcb_card + <<-XML + + + 1 + + Error + + E00027 + The transaction was unsuccessful. + + + + 3 + + P + + + 0 + + + 0 + XXXX0017 + JCB + + + 289 + This processor does not accept zero dollar authorization for this card type. + + + + + x_currency_code + USD + + + + + + XML + end end diff --git a/test/unit/gateways/banwire_test.rb b/test/unit/gateways/banwire_test.rb index b28c2411e75..5bbc177913a 100644 --- a/test/unit/gateways/banwire_test.rb +++ b/test/unit/gateways/banwire_test.rb @@ -9,10 +9,12 @@ def setup currency: 'MXN' ) - @credit_card = credit_card('5204164299999999', + @credit_card = credit_card( + '5204164299999999', month: 11, year: 2012, - verification_value: '999') + verification_value: '999' + ) @amount = 100 @options = { @@ -22,11 +24,13 @@ def setup description: 'Store purchase' } - @amex_credit_card = credit_card('375932134599999', + @amex_credit_card = credit_card( + '375932134599999', month: 3, year: 2017, first_name: 'Banwire', - last_name: 'Test Card') + last_name: 'Test Card' + ) @amex_options = { order_id: '2', email: 'test@email.com', diff --git a/test/unit/gateways/barclaycard_smartpay_test.rb b/test/unit/gateways/barclaycard_smartpay_test.rb index 9b84e216b0c..ee0c11c4da3 100644 --- a/test/unit/gateways/barclaycard_smartpay_test.rb +++ b/test/unit/gateways/barclaycard_smartpay_test.rb @@ -152,9 +152,7 @@ def test_successful_authorize_with_alternate_address def test_successful_authorize_with_house_number_and_street response = stub_comms do - @gateway.authorize(@amount, - @credit_card, - @options_with_house_number_and_street) + @gateway.authorize(@amount, @credit_card, @options_with_house_number_and_street) end.check_request do |_endpoint, data, _headers| assert_match(/billingAddress.street=Top\+Level\+Drive/, data) assert_match(/billingAddress.houseNumberOrName=1000/, data) @@ -167,9 +165,7 @@ def test_successful_authorize_with_house_number_and_street def test_successful_authorize_with_shipping_house_number_and_street response = stub_comms do - @gateway.authorize(@amount, - @credit_card, - @options_with_shipping_house_number_and_shipping_street) + @gateway.authorize(@amount, @credit_card, @options_with_shipping_house_number_and_shipping_street) end.check_request do |_endpoint, data, _headers| assert_match(/billingAddress.street=Top\+Level\+Drive/, data) assert_match(/billingAddress.houseNumberOrName=1000/, data) diff --git a/test/unit/gateways/blue_pay_test.rb b/test/unit/gateways/blue_pay_test.rb index 9385bb329ed..d1bc53db5c8 100644 --- a/test/unit/gateways/blue_pay_test.rb +++ b/test/unit/gateways/blue_pay_test.rb @@ -220,8 +220,7 @@ def test_cvv_result def test_message_from assert_equal 'CVV does not match', @gateway.send(:parse, 'STATUS=2&CVV2=N&AVS=A&MESSAGE=FAILURE').message - assert_equal 'Street address matches, but postal code does not match.', - @gateway.send(:parse, 'STATUS=2&CVV2=M&AVS=A&MESSAGE=FAILURE').message + assert_equal 'Street address matches, but postal code does not match.', @gateway.send(:parse, 'STATUS=2&CVV2=M&AVS=A&MESSAGE=FAILURE').message end def test_passing_stored_credentials_data_for_mit_transaction @@ -259,12 +258,15 @@ def test_successful_recurring @gateway.expects(:ssl_post).returns(successful_recurring_response) response = assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(@amount, @credit_card, + @gateway.recurring( + @amount, + @credit_card, billing_address: address.merge(first_name: 'Jim', last_name: 'Smith'), rebill_start_date: '1 MONTH', rebill_expression: '14 DAYS', rebill_cycles: '24', - rebill_amount: @amount * 4) + rebill_amount: @amount * 4 + ) end assert_instance_of Response, response diff --git a/test/unit/gateways/blue_snap_test.rb b/test/unit/gateways/blue_snap_test.rb index 6e8a9b02120..8553f4e9aea 100644 --- a/test/unit/gateways/blue_snap_test.rb +++ b/test/unit/gateways/blue_snap_test.rb @@ -277,6 +277,32 @@ def test_successful_purchase_with_3ds_auth assert_equal '019082915501456', response.params['network-transaction-id'] end + def test_successful_purchase_with_cit_stored_credential_fields + cit_stored_credentials = { + initiator: 'cardholder', + network_transaction_id: 'ABC123' + } + stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, @options_3ds2.merge({ stored_credential: cit_stored_credentials })) + end.check_request do |_method, _url, data| + assert_match 'SHOPPER', data + assert_match 'ABC123', data + end.respond_with(successful_purchase_with_3ds_auth_response) + end + + def test_successful_purchase_with_mit_stored_credential_fields + cit_stored_credentials = { + initiator: 'merchant', + network_transaction_id: 'QER100' + } + stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, @options_3ds2.merge({ stored_credential: cit_stored_credentials })) + end.check_request do |_method, _url, data| + assert_match 'MERCHANT', data + assert_match 'QER100', data + end.respond_with(successful_purchase_with_3ds_auth_response) + end + def test_does_not_send_3ds_auth_when_empty stub_comms(@gateway, :raw_ssl_request) do @gateway.purchase(@amount, @credit_card, @options) @@ -317,6 +343,19 @@ def test_successful_authorize assert_equal '1012082893', response.authorization end + def test_successful_authorize_with_descriptor_phone_number + options_with_phone_number = { + descriptor_phone_number: '321-321-4321' + } + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.authorize(@amount, @credit_card, options_with_phone_number) + end.check_request do |_method, _url, data| + assert_match('321-321-4321', data) + end.respond_with(successful_authorize_response) + + assert_success response + end + def test_successful_authorize_with_3ds_auth response = stub_comms(@gateway, :raw_ssl_request) do @gateway.authorize(@amount, @credit_card, @options_3ds2) @@ -416,9 +455,9 @@ def test_successful_refund assert_match item.elements['meta-description'].text, options[:transaction_meta_data][index][:meta_description] assert_match item.elements['is-visible'].text, options[:transaction_meta_data][index][:meta_is_visible] end - end.respond_with(successful_refund_response) + end.respond_with(successful_refund_without_merchant_transaction_id_response) assert_success response - assert_equal '1012082907', response.authorization + assert_equal '1061398943', response.authorization end def test_successful_refund_with_merchant_id @@ -429,6 +468,7 @@ def test_successful_refund_with_merchant_id assert_includes endpoint, "/refund/merchant/#{merchant_transaction_id}" end.respond_with(successful_refund_response) assert_success response + assert_equal '1012082907', response.authorization end def test_failed_refund @@ -1093,6 +1133,32 @@ def failed_refund_response MockResponse.failed(body, 400) end + def successful_refund_without_merchant_transaction_id_response + MockResponse.succeeded <<-XML + + + 1061398943 + 1.00 + 0.05 + + + refundedItems + 1552,8832 + Refunded Items + false + + + Number2 + KTD + Metadata 2 + true + + + Refund for order #1992 + + XML + end + def successful_void_response MockResponse.succeeded <<-XML diff --git a/test/unit/gateways/bogus_test.rb b/test/unit/gateways/bogus_test.rb index 4564a4d3096..301ed2ddfa6 100644 --- a/test/unit/gateways/bogus_test.rb +++ b/test/unit/gateways/bogus_test.rb @@ -96,6 +96,17 @@ def test_void end end + def test_verify + assert @gateway.verify(credit_card(CC_SUCCESS_PLACEHOLDER)).success? + response = @gateway.verify(credit_card(CC_FAILURE_PLACEHOLDER)) + refute response.success? + assert_equal Gateway::STANDARD_ERROR_CODE[:processing_error], response.error_code + e = assert_raises(ActiveMerchant::Billing::Error) do + @gateway.verify(credit_card('123')) + end + assert_equal('Bogus Gateway: Use CreditCard number ending in 1 for success, 2 for exception and anything else for error', e.message) + end + def test_store assert @gateway.store(credit_card(CC_SUCCESS_PLACEHOLDER)).success? response = @gateway.store(credit_card(CC_FAILURE_PLACEHOLDER)) diff --git a/test/unit/gateways/borgun_test.rb b/test/unit/gateways/borgun_test.rb index 506b78c88cf..8550b24eb79 100644 --- a/test/unit/gateways/borgun_test.rb +++ b/test/unit/gateways/borgun_test.rb @@ -56,6 +56,48 @@ def test_authorize_and_capture assert_success capture end + def test_failed_preauth_3ds + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ redirect_url: 'http://localhost/index.html', apply_3d_secure: '1', sale_description: 'product description' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/MerchantReturnURL>#{@options[:redirect_url]}/, data) + assert_match(/SaleDescription>#{@options[:sale_description]}/, data) + assert_match(/TrCurrencyExponent>2/, data) + end.respond_with(failed_get_3ds_authentication_response) + + assert_failure response + assert_equal response.message, 'Exception in PostEnrollmentRequest.' + assert response.authorization.blank? + end + + def test_successful_preauth_3ds + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ redirect_url: 'http://localhost/index.html', apply_3d_secure: '1', sale_description: 'product description' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/MerchantReturnURL>#{@options[:redirect_url]}/, data) + assert_match(/SaleDescription>#{@options[:sale_description]}/, data) + assert_match(/TrCurrencyExponent>2/, data) + end.respond_with(successful_get_3ds_authentication_response) + + assert_success response + assert !response.params['redirecttoacsform'].blank? + assert !response.params['acsformfields_actionurl'].blank? + assert !response.params['acsformfields_pareq'].blank? + assert !response.params['threedsmessageid'].blank? + assert response.authorization.blank? + end + + def test_successful_purchase_after_3ds + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ three_ds_message_id: '98324_zzi_1234353' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/ThreeDSMessageId>#{@options[:three_ds_message_id]}/, data) + assert_match(/TrCurrencyExponent>0/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_authorize_airline_data # itinerary data abbreviated for brevity passenger_itinerary_data = { @@ -312,6 +354,55 @@ def successful_void_response ) end + def successful_get_3ds_authentication_response + <<~RESPONSE + + + + + + 0 + + + 1000 + 23 + 23 + 23_20201408041400003 + 9 + ToberedirectedtoACS + Y + 3C21444F43545950452068746D6C2053595354454D202261626F75743A6C65676163792D636F6D706174223E0D0D0A3C68746D6C20636C6173733D226E6F2D6A7322206C616E673D22656E2220786D6C6E733D22687474703A2F2F7777772E77332E6F72672F313939392F7868746D6C223E0D0D0A3C686561643E0D0D0A3C6D65746120687474702D65717569763D22436F6E74656E742D547970652220636F6E74656E743D22746578742F68746D6C3B20636861727365743D7574662D38222F3E0D0D0A3C6D65746120636861727365743D227574662D38222F3E0D0D0A3C7469746C653E3344205365637572652050726F63657373696E673C2F7469746C653E0D0D0A3C6C696E6B20687265663D2268747470733A2F2F6D70692E626F7267756E2E69732F6D647061796D70692F7374617469632F6D70692E637373222072656C3D227374796C6573686565742220747970653D22746578742F637373222F3E0D0D0A3C2F686561643E0D0D0A3C626F64793E0D0D0A3C6469762069643D226D61696E223E0D0D0A3C6469762069643D22636F6E74656E74223E0D0D0A3C6469762069643D226F72646572223E0D0D0A3C68323E3344205365637572652050726F63657373696E673C2F68323E0D0D0A3C696D67207372633D2268747470733A2F2F6D70692E626F7267756E2E69732F6D647061796D70692F7374617469632F7072656C6F616465722E6769662220616C743D22506C6561736520776169742E2E222F3E0D0D0A3C696D67207372633D2268747470733A2F2F6D70692E626F7267756E2E69732F6D647061796D70692F7374617469632F6D635F6964636865636B5F68727A5F6C74645F706F735F31303370782E706E672220616C743D224D61737465724361726420494420436865636B222F3E0D0D0A3C6469762069643D22666F726D646976223E0D0D0A3C73637269707420747970653D22746578742F6A617661736372697074223E0D0D0A66756E6374696F6E2068696465416E645375626D697454696D656428666F726D6964290D0D0A7B0D0D0A7661722074696D65723D73657454696D656F7574282268696465416E645375626D69742827222B666F726D69642B2227293B222C313030293B0D0D0A7D0D0D0A0D0D0A66756E6374696F6E2068696465416E645375626D697428666F726D6964290D0D0A7B0D0D0A76617220666F726D783D646F63756D656E742E676574456C656D656E744279496428666F726D6964293B0D0D0A0969662028666F726D78213D6E756C6C290D0D0A097B0D0D0A09666F726D782E7374796C652E7669736962696C6974793D2268696464656E223B0D0D0A09666F726D782E7375626D697428293B0D0D0A097D0D0D0A7D0D0D0A3C2F7363726970743E0D0D0A3C6469763E0D0D0A3C666F726D2069643D22776562666F726D3022206E616D653D2272656432414353763122206D6574686F643D22504F53542220616374696F6E3D2268747470733A2F2F616373312E3364732E6D6F646972756D2E636F6D2F6D647061796163732F706172657122206163636570745F636861727365743D225554462D38223E0D0D0A3C696E70757420747970653D2268696464656E22206E616D653D225F636861727365745F222076616C75653D225554462D38222F3E0D0D0A3C696E70757420747970653D2268696464656E22206E616D653D225061526571222076616C75653D22654A785655753175676A4155665258692F396D57723443354E7346684D724C676D4F7746574C6C427A437861697445392F56715575663237353979766E6E4D4C487A75466D4A596F426F556363757A37716B476E725A657A5976764F614F4146455976634759636932654B4A77786C5633336153737A6D647530416D614471563246565363366A45615A5674654F417A502F4B4133434563554755706A39306744434D6679413243724137495635317142756B384F586D52505A56765365466F374855724779426A486B5133534B32753341764D79676E416F4C37345475766A6770445063634B383759465946736A6A4F6356676F39354D756251317A2F664A4E552B5449452B627932612F706E6D6166322F53684F587065676E45566B42646165517564536D4E61655377634D4838425456435268367167313350732F4C56595A51616554634D5237736D75594578385A6341343635434B53594645774B384844754A707349302F4D5A51597939346F627036454E6F70555A31626755625A53414E354348702B7357344C6259786B4E4B4978382B4B5157636448796735766A5538756F3279636267455132305475787954336535766F337A2F34415552317277553D222F3E0D0D0A3C696E70757420747970653D2268696464656E22206E616D653D224D44222076616C75653D2232335F3230323031343038303431343030303033222F3E0D0D0A3C696E70757420747970653D2268696464656E22206E616D653D225465726D55726C222076616C75653D22687474703A2F2F6C6F63616C686F73742F696E6465782E68746D6C222F3E0D0D0A3C696E70757420747970653D227375626D697422206E616D653D227375626D697442746E222076616C75653D22506C6561736520636C69636B206865726520746F20636F6E74696E7565222F3E0D0D0A3C2F666F726D3E0D0D0A3C2F6469763E0D0D0A3C2F6469763E0D0D0A3C73637269707420747970653D22746578742F6A617661736372697074223E0D0D0A09090968696465416E645375626D697454696D65642827776562666F726D3027293B0D0D0A09093C2F7363726970743E0D0D0A3C6E6F7363726970743E0D0D0A3C64697620616C69676E3D2263656E746572223E0D0D0A3C623E4A617661736372697074206973207475726E6564206F6666206F72206E6F7420737570706F72746564213C2F623E0D0D0A3C62722F3E0D0D0A3C2F6469763E0D0D0A3C2F6E6F7363726970743E0D0D0A3C2F6469763E0D0D0A3C6469762069643D22636F6E74656E742D666F6F746572223E0D0D0A3C62722F3E0D0D0A3C696D67206865696768743D22323022207372633D2268747470733A2F2F6D70692E626F7267756E2E69732F6D647061796D70692F7374617469632F706F77657265642D62792D6D6F646972756D2E7376672220616C743D22506F7765726564206279204D6F646972756D222F3E0D0D0A3C2F6469763E0D0D0A3C2F6469763E0D0D0A3C2F6469763E0D0D0A3C2F626F64793E0D0D0A3C2F68746D6C3E0D0D0A + + + https://acs1.3ds.modirum.com/mdpayacs/pareq + eJxVUu1ugjAUfRXi/9mWr4C5NsFhMrLgmOwFWLlBzCxaitE9/VqUuf2759yvnnMLHzuFmJYoBoUccuz7qkGnrZezYvvOaOAFEYvcGYci2eKJwxlV33aSszmdu0AmaDqV2FVSc6jEaZVteOAzP/KA3CEcUGUpj90gDCMfyA2CrA7IV51qBuk8OXmRPZVvSeFo7HUrGyBjHkQ3SK2u3AvMygnAoL74TuvjgpDPccK87YFYFsjjOcVgo95MubQ1z/fJNU+TIE+by2a/pnmaf2/ShOXpegnEVkBdaeQudSmNaeSwcMH8BTVCRh6qg13Ps/LVYZQaeTcMR7smuYEx8ZcA465CKSYFEwK8HDuJpsI0/MZQYy94obp6ENopUZ1bgUbZSAN5CHp+sW4LbYxkNKIx8+KQWcdHyg5vjU8uo2ycbgEQ20TuxyT3e5vo3z/4AUR1rwU= + http://localhost/index.html + + + + + RESPONSE + end + + def failed_get_3ds_authentication_response + %( + + + + + <?xml version="1.0" encoding="iso-8859-1"?> + <get3DSAuthenticationReply> + <Status> + <ResultCode>30</ResultCode> + <ResultText>MPI returns error</ResultText> + <ErrorMessage>Exception in PostEnrollmentRequest.</ErrorMessage> + </Status> + </get3DSAuthenticationReply> + + + ) + end + def transcript <<-PRE_SCRUBBED <- "POST /ws/Heimir.pub.ws:Authorization HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic yyyyyyyyyyyyyyyyyyyyyyyyyy==\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: gateway01.borgun.is\r\nContent-Length: 1220\r\n\r\n" diff --git a/test/unit/gateways/braintree_blue_test.rb b/test/unit/gateways/braintree_blue_test.rb index 8cee7563fd4..14662a8e1bc 100644 --- a/test/unit/gateways/braintree_blue_test.rb +++ b/test/unit/gateways/braintree_blue_test.rb @@ -94,6 +94,16 @@ def test_capture_transaction assert_equal true, response.test end + def test_partial_capture_transaction + Braintree::TransactionGateway.any_instance.expects(:submit_for_partial_settlement). + returns(braintree_result(id: 'capture_transaction_id')) + + response = @gateway.capture(100, 'transaction_id', { partial_capture: true }) + + assert_equal 'capture_transaction_id', response.authorization + assert_equal true, response.test + end + def test_refund_transaction Braintree::TransactionGateway.any_instance.expects(:refund). returns(braintree_result(id: 'refund_transaction_id')) @@ -210,6 +220,31 @@ def test_service_fee_amount_can_be_specified @gateway.authorize(100, credit_card('41111111111111111111'), service_fee_amount: '2.31') end + def test_venmo_profile_id_can_be_specified + Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| + (params[:options][:venmo][:profile_id] == 'profile_id') + end.returns(braintree_result) + + @gateway.authorize(100, credit_card('41111111111111111111'), venmo_profile_id: 'profile_id') + end + + def test_customer_has_default_payment_method + options = { + payment_method_nonce: 'fake-paypal-future-nonce', + store: true, + device_data: 'device_data', + paypal: { + paypal_flow_type: 'checkout_with_vault' + } + } + + Braintree::TransactionGateway.any_instance.expects(:sale).returns(braintree_result(paypal: { implicitly_vaulted_payment_method_token: 'abc123' })) + + Braintree::CustomerGateway.any_instance.expects(:update).with(nil, { default_payment_method_token: 'abc123' }).returns(nil) + + @gateway.authorize(100, 'fake-paypal-future-nonce', options) + end + def test_risk_data_can_be_specified risk_data = { customer_browser: 'User-Agent Header', @@ -229,6 +264,15 @@ def test_hold_in_escrow_can_be_specified @gateway.authorize(100, credit_card('41111111111111111111'), hold_in_escrow: true) end + def test_paypal_options_can_be_specified + Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| + (params[:options][:paypal][:custom_field] == 'abc') + (params[:options][:paypal][:description] == 'shoes') + end.returns(braintree_result) + + @gateway.authorize(100, credit_card('4111111111111111'), paypal_custom_field: 'abc', paypal_description: 'shoes') + end + def test_merchant_account_id_absent_if_not_provided Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| not params.has_key?(:merchant_account_id) @@ -702,21 +746,34 @@ def test_three_d_secure_pass_thru_handling_version_1 end def test_three_d_secure_pass_thru_handling_version_2 - Braintree::TransactionGateway. - any_instance. - expects(:sale). - with(has_entries(three_d_secure_pass_thru: has_entries( - three_d_secure_version: '2.0', + three_ds_expectation = { + three_d_secure_version: '2.0', + cavv: 'cavv', + eci_flag: 'eci', + ds_transaction_id: 'trans_id', + cavv_algorithm: 'algorithm', + directory_response: 'directory', + authentication_response: 'auth' + } + + Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| + (params[:sca_exemption] == 'low_value') + (params[:three_d_secure_pass_thru] == three_ds_expectation) + end.returns(braintree_result) + + options = { + three_ds_exemption_type: 'low_value', + three_d_secure: { + version: '2.0', cavv: 'cavv', - eci_flag: 'eci', + eci: 'eci', ds_transaction_id: 'trans_id', cavv_algorithm: 'algorithm', - directory_response: 'directory', - authentication_response: 'auth' - ))). - returns(braintree_result) - - @gateway.purchase(100, credit_card('41111111111111111111'), three_d_secure: { version: '2.0', cavv: 'cavv', eci: 'eci', ds_transaction_id: 'trans_id', cavv_algorithm: 'algorithm', directory_response_status: 'directory', authentication_response_status: 'auth' }) + directory_response_status: 'directory', + authentication_response_status: 'auth' + } + } + @gateway.purchase(100, credit_card('41111111111111111111'), options) end def test_three_d_secure_pass_thru_some_fields @@ -901,14 +958,17 @@ def test_successful_purchase_with_travel_data (params[:industry][:data][:lodging_name] == 'Best Hotel Ever') end.returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), + @gateway.purchase( + 100, + credit_card('41111111111111111111'), travel_data: { travel_package: 'flight', departure_date: '2050-07-22', lodging_check_in_date: '2050-07-22', lodging_check_out_date: '2050-07-25', lodging_name: 'Best Hotel Ever' - }) + } + ) end def test_successful_purchase_with_lodging_data @@ -920,13 +980,16 @@ def test_successful_purchase_with_lodging_data (params[:industry][:data][:room_rate] == '80.00') end.returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), + @gateway.purchase( + 100, + credit_card('41111111111111111111'), lodging_data: { folio_number: 'ABC123', check_in_date: '2050-12-22', check_out_date: '2050-12-25', room_rate: '80.00' - }) + } + ) end def test_apple_pay_card @@ -949,17 +1012,19 @@ def test_apple_pay_card ). returns(braintree_result(id: 'transaction_id')) - credit_card = network_tokenization_credit_card('4111111111111111', + credit_card = network_tokenization_credit_card( + '4111111111111111', brand: 'visa', transaction_id: '123', eci: '05', - payment_cryptogram: '111111111100cryptogram') + payment_cryptogram: '111111111100cryptogram' + ) response = @gateway.authorize(100, credit_card, test: true, order_id: '1') assert_equal 'transaction_id', response.authorization end - def test_android_pay_card + def test_google_pay_card Braintree::TransactionGateway.any_instance.expects(:sale). with( amount: '1.00', @@ -981,18 +1046,20 @@ def test_android_pay_card ). returns(braintree_result(id: 'transaction_id')) - credit_card = network_tokenization_credit_card('4111111111111111', + credit_card = network_tokenization_credit_card( + '4111111111111111', brand: 'visa', eci: '05', payment_cryptogram: '111111111100cryptogram', - source: :android_pay, - transaction_id: '1234567890') + source: :google_pay, + transaction_id: '1234567890' + ) response = @gateway.authorize(100, credit_card, test: true, order_id: '1') assert_equal 'transaction_id', response.authorization end - def test_google_pay_card + def test_network_token_card Braintree::TransactionGateway.any_instance.expects(:sale). with( amount: '1.00', @@ -1001,25 +1068,24 @@ def test_google_pay_card first_name: 'Longbob', last_name: 'Longsen' }, options: { store_in_vault: false, submit_for_settlement: nil, hold_in_escrow: nil }, custom_fields: nil, - google_pay_card: { + credit_card: { number: '4111111111111111', expiration_month: '09', expiration_year: (Time.now.year + 1).to_s, - cryptogram: '111111111100cryptogram', - google_transaction_id: '1234567890', - source_card_type: 'visa', - source_card_last_four: '1111', - eci_indicator: '05' + cardholder_name: 'Longbob Longsen', + network_tokenization_attributes: { + cryptogram: '111111111100cryptogram', + ecommerce_indicator: '05' + } } ). returns(braintree_result(id: 'transaction_id')) credit_card = network_tokenization_credit_card('4111111111111111', - brand: 'visa', - eci: '05', - payment_cryptogram: '111111111100cryptogram', - source: :google_pay, - transaction_id: '1234567890') + brand: 'visa', + eci: '05', + source: :network_token, + payment_cryptogram: '111111111100cryptogram') response = @gateway.authorize(100, credit_card, test: true, order_id: '1') assert_equal 'transaction_id', response.authorization @@ -1315,6 +1381,14 @@ def test_returns_error_on_authorize_when_passing_a_bank_account assert_equal 'Direct bank account transactions are not supported. Bank accounts must be successfully stored before use.', response.message end + def test_returns_error_on_general_credit_when_passing_a_bank_account + bank_account = check({ account_number: '1000000002', routing_number: '011000015' }) + response = @gateway.credit(100, bank_account, {}) + + assert_failure response + assert_equal 'Direct bank account transactions are not supported. Bank accounts must be successfully stored before use.', response.message + end + def test_error_on_store_bank_account_without_a_mandate options = { billing_address: { diff --git a/test/unit/gateways/card_connect_test.rb b/test/unit/gateways/card_connect_test.rb index 7e8680513df..7188dea17dc 100644 --- a/test/unit/gateways/card_connect_test.rb +++ b/test/unit/gateways/card_connect_test.rb @@ -14,6 +14,96 @@ def setup billing_address: address, description: 'Store Purchase' } + + @three_ds_secure = { + version: '2.0', + cavv: 'AJkBByEyYgAAAASwgmEodQAAAAA=', + eci: '05', + xid: '3875d372-d96d-412a-a806-5ac367d095b1' + } + end + + def test_three_ds_2_object_construction + post = {} + @three_ds_secure[:ds_transaction_id] = '3875d372-d96d-412a-a806-5ac367d095b1' + @options[:three_d_secure] = @three_ds_secure + + @gateway.send(:add_three_ds_mpi_data, post, @options) + three_ds_options = @options[:three_d_secure] + assert_equal three_ds_options[:cavv], post[:securevalue] + assert_equal three_ds_options[:eci], post[:secureflag] + assert_equal three_ds_options[:ds_transaction_id], post[:securedstid] + end + + def test_purchase_with_three_ds + @options[:three_d_secure] = @three_ds_secure + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + three_ds_params = JSON.parse(data)['three_dsecure'] + assert_equal 'AJkBByEyYgAAAASwgmEodQAAAAA=', three_ds_params['cavv'] + assert_equal '05', three_ds_params['eci'] + assert_equal '3875d372-d96d-412a-a806-5ac367d095b1', three_ds_params['xid'] + end.respond_with(successful_purchase_response) + end + + def test_initial_purchase_with_stored_credential + stored_credential_options = { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'merchant' + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + end.check_request do |_verb, _url, data, _headers| + request = JSON.parse(data) + assert_equal request['cof'], 'M' + assert_equal request['cofscheduled'], 'Y' + assert_equal request['cofpermission'], 'Y' + end.respond_with(successful_purchase_response) + end + + def test_subsequent_purchase_with_stored_credential + stored_credential_options = { + initial_transaction: false, + reason_type: 'recurring', + initiator: 'cardholder' + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + end.check_request do |_verb, _url, data, _headers| + request = JSON.parse(data) + assert_equal request['cof'], 'C' + assert_equal request['cofscheduled'], 'Y' + assert_equal request['cofpermission'], 'N' + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_ecomind + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ ecomind: 't' })) + end.check_request do |_verb, _url, data, _headers| + request = JSON.parse(data) + assert_equal request['ecomind'], 'T' + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_recurring + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ recurring: true })) + end.check_request do |_verb, _url, data, _headers| + request = JSON.parse(data) + assert_equal request['ecomind'], 'R' + end.respond_with(successful_purchase_response) + end + + def test_purchase_without_recurring + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ recurring: false })) + end.check_request do |_verb, _url, data, _headers| + request = JSON.parse(data) + assert_equal request['ecomind'], 'E' + end.respond_with(successful_purchase_response) end def test_allow_domains_without_ports diff --git a/test/unit/gateways/card_stream_test.rb b/test/unit/gateways/card_stream_test.rb index 256a740184f..f648a960a42 100644 --- a/test/unit/gateways/card_stream_test.rb +++ b/test/unit/gateways/card_stream_test.rb @@ -9,11 +9,13 @@ def setup shared_secret: 'secret' ) - @visacreditcard = credit_card('4929421234600821', + @visacreditcard = credit_card( + '4929421234600821', month: '12', year: '2014', verification_value: '356', - brand: :visa) + brand: :visa + ) @visacredit_options = { billing_address: { @@ -27,6 +29,18 @@ def setup description: 'AM test purchase' } + @visacredit_three_ds_options = { + threeds_required: true, + three_ds_version: '2.1.0', + three_d_secure: { + enrolled: 'true', + authentication_response_status: 'Y', + eci: '05', + cavv: 'Y2FyZGluYWxjb21tZXJjZWF1dGg', + xid: '362DF058-6061-47F1-A504-CACCBDF422B7' + } + } + @visacredit_descriptor_options = { billing_address: { address1: 'Flat 6, Primrose Rise', @@ -39,15 +53,15 @@ def setup dynamic_descriptor: 'product' } - @amex = credit_card('374245455400001', + @amex = credit_card( + '374245455400001', month: '12', year: 2014, verification_value: '4887', - brand: :american_express) + brand: :american_express + ) - @declined_card = credit_card('4000300011112220', - month: '9', - year: '2014') + @declined_card = credit_card('4000300011112220', month: '9', year: '2014') end def test_successful_visacreditcard_authorization @@ -311,6 +325,40 @@ def test_default_3dsecure_required end.respond_with(successful_purchase_response) end + def test_3ds2_data + stub_comms do + @gateway.purchase(142, @visacreditcard, @visacredit_options.merge(@visacredit_three_ds_options)) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match(/threeDSRequired=Y/, data) + assert_match(/threeDSEnrolled=Y/, data) + assert_match(/threeDSAuthenticated=Y/, data) + assert_match(/threeDSECI=05/, data) + assert_match(/threeDSCAVV=Y2FyZGluYWxjb21tZXJjZWF1dGg/, data) + assert_match(/threeDSXID=362DF058-6061-47F1-A504-CACCBDF422B7/, data) + end + end + + def test_3ds2_not_enrolled + stub_comms do + @visacredit_three_ds_options[:three_d_secure][:enrolled] = 'false' + @gateway.purchase(142, @visacreditcard, @visacredit_options.merge(@visacredit_three_ds_options)) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match(/threeDSRequired=Y/, data) + assert_match(/threeDSEnrolled=N/, data) + end + end + + def test_3ds2_not_authenticated + stub_comms do + @visacredit_three_ds_options[:three_d_secure][:authentication_response_status] = 'N' + @gateway.purchase(142, @visacreditcard, @visacredit_options.merge(@visacredit_three_ds_options)) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match(/threeDSRequired=Y/, data) + assert_match(/threeDSEnrolled=Y/, data) + assert_match(/threeDSAuthenticated=N/, data) + end + end + def test_transcript_scrubbing assert_equal scrubbed_transcript, @gateway.scrub(transcript) end diff --git a/test/unit/gateways/checkout_v2_test.rb b/test/unit/gateways/checkout_v2_test.rb index 2869a81b11c..309b32587e0 100644 --- a/test/unit/gateways/checkout_v2_test.rb +++ b/test/unit/gateways/checkout_v2_test.rb @@ -1,5 +1,15 @@ require 'test_helper' +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class CheckoutV2Gateway + def setup_access_token + '12345678' + end + end + end +end + class CheckoutV2Test < Test::Unit::TestCase include CommStub @@ -7,13 +17,18 @@ def setup @gateway = CheckoutV2Gateway.new( secret_key: '1111111111111' ) - + @gateway_oauth = CheckoutV2Gateway.new({ client_id: 'abcd', client_secret: '1234' }) + @gateway_api = CheckoutV2Gateway.new({ + secret_key: '1111111111111', + public_key: '2222222222222' + }) @credit_card = credit_card @amount = 100 + @token = '2MPedsuenG2o8yFfrsdOBWmOuEf' end def test_successful_purchase - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card) end.respond_with(successful_purchase_response) @@ -23,7 +38,7 @@ def test_successful_purchase end def test_successful_purchase_includes_avs_result - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card) end.respond_with(successful_purchase_response) @@ -34,7 +49,7 @@ def test_successful_purchase_includes_avs_result end def test_successful_purchase_includes_cvv_result - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card) end.respond_with(successful_purchase_response) @@ -46,9 +61,9 @@ def test_successful_purchase_using_vts_network_token_without_eci '4242424242424242', { source: :network_token, brand: 'visa' } ) - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, network_token) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| request_data = JSON.parse(data) assert_equal(request_data['source']['type'], 'network_token') @@ -63,14 +78,61 @@ def test_successful_purchase_using_vts_network_token_without_eci assert response.test? end + def test_successful_passing_processing_channel_id + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { processing_channel_id: '123456abcde' }) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + assert_equal(request_data['processing_channel_id'], '123456abcde') + end.respond_with(successful_purchase_response) + end + + def test_successful_passing_incremental_authorization + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, { incremental_authorization: 'abcd1234' }) + end.check_request do |_method, endpoint, _data, _headers| + assert_include endpoint, 'abcd1234' + end.respond_with(successful_incremental_authorize_response) + + assert_success response + end + + def test_successful_passing_authorization_type + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { authorization_type: 'Estimated' }) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + assert_equal(request_data['authorization_type'], 'Estimated') + end.respond_with(successful_purchase_response) + end + + def test_successful_passing_exemption_and_challenge_indicator + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { execute_threed: true, exemption: 'no_preference', challenge_indicator: 'trusted_listing' }) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + assert_equal(request_data['3ds']['exemption'], 'no_preference') + assert_equal(request_data['3ds']['challenge_indicator'], 'trusted_listing') + end.respond_with(successful_purchase_response) + end + + def test_successful_passing_capture_type + stub_comms(@gateway, :ssl_request) do + @gateway.capture(@amount, 'abc', { capture_type: 'NonFinal' }) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + assert_equal(request_data['capture_type'], 'NonFinal') + end.respond_with(successful_capture_response) + end + def test_successful_purchase_using_vts_network_token_with_eci network_token = network_tokenization_credit_card( '4242424242424242', { source: :network_token, brand: 'visa', eci: '06' } ) - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, network_token) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| request_data = JSON.parse(data) assert_equal(request_data['source']['type'], 'network_token') @@ -90,9 +152,9 @@ def test_successful_purchase_using_mdes_network_token '5436031030606378', { source: :network_token, brand: 'master' } ) - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, network_token) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| request_data = JSON.parse(data) assert_equal(request_data['source']['type'], 'network_token') @@ -112,9 +174,9 @@ def test_successful_purchase_using_apple_pay_network_token '4242424242424242', { source: :apple_pay, eci: '05', payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA' } ) - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, network_token) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| request_data = JSON.parse(data) assert_equal(request_data['source']['type'], 'network_token') @@ -134,9 +196,9 @@ def test_successful_purchase_using_android_pay_network_token '4242424242424242', { source: :android_pay, eci: '05', payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA' } ) - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, network_token) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| request_data = JSON.parse(data) assert_equal(request_data['source']['type'], 'network_token') @@ -156,9 +218,9 @@ def test_successful_purchase_using_google_pay_network_token '4242424242424242', { source: :google_pay, eci: '05', payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA' } ) - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, network_token) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| request_data = JSON.parse(data) assert_equal(request_data['source']['type'], 'network_token') @@ -178,9 +240,9 @@ def test_successful_purchase_using_google_pay_pan_only_network_token '4242424242424242', { source: :google_pay } ) - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, network_token) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| request_data = JSON.parse(data) assert_equal(request_data['source']['type'], 'network_token') @@ -195,8 +257,20 @@ def test_successful_purchase_using_google_pay_pan_only_network_token assert response.test? end + def test_successful_render_for_oauth + processing_channel_id = 'abcd123' + response = stub_comms(@gateway_oauth, :ssl_request) do + @gateway_oauth.purchase(@amount, @credit_card, { processing_channel_id: processing_channel_id }) + end.check_request do |_method, _endpoint, data, headers| + request = JSON.parse(data) + assert_equal headers['Authorization'], 'Bearer 12345678' + assert_equal request['processing_channel_id'], processing_channel_id + end.respond_with(successful_purchase_response) + assert_success response + end + def test_successful_authorize_includes_avs_result - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, @credit_card) end.respond_with(successful_authorize_response) @@ -207,7 +281,7 @@ def test_successful_authorize_includes_avs_result end def test_successful_authorize_includes_cvv_result - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, @credit_card) end.respond_with(successful_authorize_response) @@ -215,17 +289,28 @@ def test_successful_authorize_includes_cvv_result end def test_purchase_with_additional_fields - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, { descriptor_city: 'london', descriptor_name: 'sherlock' }) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/"billing_descriptor\":{\"name\":\"sherlock\",\"city\":\"london\"}/, data) end.respond_with(successful_purchase_response) assert_success response end + def test_successful_purchase_passing_metadata_with_mada_card_type + @credit_card.brand = 'mada' + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card) + end.check_request do |_method, _endpoint, data, _headers| + request_data = JSON.parse(data) + assert_equal(request_data['metadata']['udf1'], 'mada') + end.respond_with(successful_purchase_response) + end + def test_failed_purchase - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card) end.respond_with(failed_purchase_response) assert_failure response @@ -233,14 +318,14 @@ def test_failed_purchase end def test_successful_authorize_and_capture - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, @credit_card) end.respond_with(successful_authorize_response) assert_success response assert_equal 'pay_fj3xswqe3emuxckocjx6td73ni', response.authorization - capture = stub_comms do + capture = stub_comms(@gateway, :ssl_request) do @gateway.capture(@amount, response.authorization) end.respond_with(successful_capture_response) @@ -248,29 +333,25 @@ def test_successful_authorize_and_capture end def test_successful_authorize_and_capture_with_additional_options - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do options = { card_on_file: true, transaction_indicator: 2, previous_charge_id: 'pay_123', - processing_channel_id: 'pc_123', - marketplace: { - sub_entity_id: 'ent_123' - } + processing_channel_id: 'pc_123' } @gateway.authorize(@amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(%r{"stored":"true"}, data) assert_match(%r{"payment_type":"Recurring"}, data) assert_match(%r{"previous_payment_id":"pay_123"}, data) assert_match(%r{"processing_channel_id":"pc_123"}, data) - assert_match(/"marketplace\":{\"sub_entity_id\":\"ent_123\"}/, data) end.respond_with(successful_authorize_response) assert_success response assert_equal 'pay_fj3xswqe3emuxckocjx6td73ni', response.authorization - capture = stub_comms do + capture = stub_comms(@gateway, :ssl_request) do @gateway.capture(@amount, response.authorization) end.respond_with(successful_capture_response) @@ -278,15 +359,16 @@ def test_successful_authorize_and_capture_with_additional_options end def test_successful_purchase_with_stored_credentials - initial_response = stub_comms do + initial_response = stub_comms(@gateway, :ssl_request) do initial_options = { stored_credential: { + initiator: 'cardholder', initial_transaction: true, reason_type: 'installment' } } @gateway.purchase(@amount, @credit_card, initial_options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(%r{"payment_type":"Recurring"}, data) assert_match(%r{"merchant_initiated":false}, data) end.respond_with(successful_purchase_initial_stored_credential_response) @@ -295,7 +377,7 @@ def test_successful_purchase_with_stored_credentials assert_equal 'pay_7jcf4ovmwnqedhtldca3fjli2y', initial_response.params['id'] network_transaction_id = initial_response.params['id'] - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do options = { stored_credential: { initial_transaction: false, @@ -304,17 +386,67 @@ def test_successful_purchase_with_stored_credentials } } @gateway.purchase(@amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| - assert_match(%r{"previous_payment_id":"pay_7jcf4ovmwnqedhtldca3fjli2y"}, data) - assert_match(%r{"source.stored":true}, data) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['previous_payment_id'], 'pay_7jcf4ovmwnqedhtldca3fjli2y' + assert_equal request['source']['stored'], true + end.respond_with(successful_purchase_using_stored_credential_response) + + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_stored_credentials_merchant_initiated_transaction_id + response = stub_comms(@gateway, :ssl_request) do + options = { + stored_credential: { + initial_transaction: false + }, + merchant_initiated_transaction_id: 'pay_7jcf4ovmwnqedhtldca3fjli2y' + } + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['previous_payment_id'], 'pay_7jcf4ovmwnqedhtldca3fjli2y' + assert_equal request['source']['stored'], true end.respond_with(successful_purchase_using_stored_credential_response) assert_success response assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_extra_customer_data + stub_comms(@gateway, :ssl_request) do + options = { + phone_country_code: '1', + billing_address: address + } + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['source']['phone']['number'], '(555)555-5555' + assert_equal request['source']['phone']['country_code'], '1' + assert_equal request['customer']['name'], 'Longbob Longsen' + end.respond_with(successful_purchase_response) + end + + def test_no_customer_name_included_in_token_purchase + stub_comms(@gateway, :ssl_request) do + options = { + phone_country_code: '1', + billing_address: address + } + @gateway.purchase(@amount, @token, options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['source']['phone']['number'], '(555)555-5555' + assert_equal request['source']['phone']['country_code'], '1' + refute_includes data, 'name' + end.respond_with(successful_purchase_response) + end + def test_successful_purchase_with_metadata - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do options = { metadata: { coupon_code: 'NY2018', @@ -322,7 +454,7 @@ def test_successful_purchase_with_metadata } } @gateway.purchase(@amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(%r{"coupon_code":"NY2018"}, data) assert_match(%r{"partner_id":"123989"}, data) end.respond_with(successful_purchase_using_stored_credential_response) @@ -330,8 +462,19 @@ def test_successful_purchase_with_metadata assert_success response end + def test_optional_idempotency_key_header + stub_comms(@gateway, :ssl_request) do + options = { + idempotency_key: 'test123' + } + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _url, _data, headers| + assert_equal 'test123', headers['Cko-Idempotency-Key'] + end.respond_with(successful_authorize_response) + end + def test_successful_authorize_and_capture_with_metadata - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do options = { metadata: { coupon_code: 'NY2018', @@ -339,7 +482,7 @@ def test_successful_authorize_and_capture_with_metadata } } @gateway.authorize(@amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(%r{"coupon_code":"NY2018"}, data) assert_match(%r{"partner_id":"123989"}, data) end.respond_with(successful_authorize_response) @@ -347,7 +490,7 @@ def test_successful_authorize_and_capture_with_metadata assert_success response assert_equal 'pay_fj3xswqe3emuxckocjx6td73ni', response.authorization - capture = stub_comms do + capture = stub_comms(@gateway, :ssl_request) do @gateway.capture(@amount, response.authorization) end.respond_with(successful_capture_response) @@ -355,14 +498,14 @@ def test_successful_authorize_and_capture_with_metadata end def test_moto_transaction_is_properly_set - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do options = { metadata: { manual_entry: true } } @gateway.authorize(@amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(%r{"payment_type":"MOTO"}, data) end.respond_with(successful_authorize_response) @@ -370,13 +513,13 @@ def test_moto_transaction_is_properly_set end def test_3ds_passed - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do options = { execute_threed: true, callback_url: 'https://www.example.com' } @gateway.authorize(@amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(%r{"success_url"}, data) assert_match(%r{"failure_url"}, data) end.respond_with(successful_authorize_response) @@ -388,7 +531,16 @@ def test_successful_verify_payment response = stub_comms(@gateway, :ssl_request) do @gateway.verify_payment('testValue') end.respond_with(successful_verify_payment_response) + assert_success response + end + def test_verify_payment_request + response = stub_comms(@gateway, :ssl_request) do + @gateway.verify_payment('testValue') + end.check_request do |_method, endpoint, data, _headers| + assert_equal nil, data + assert_equal 'https://api.sandbox.checkout.com/payments/testValue', endpoint + end.respond_with(successful_verify_payment_response) assert_success response end @@ -401,7 +553,7 @@ def test_failed_verify_payment end def test_successful_authorize_and_capture_with_3ds - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do options = { execute_threed: true, attempt_n3d: true, @@ -409,7 +561,8 @@ def test_successful_authorize_and_capture_with_3ds version: '1.0.2', eci: '05', cryptogram: '1234', - xid: '1234' + xid: '1234', + authentication_response_status: 'Y' } } @gateway.authorize(@amount, @credit_card, options) @@ -418,7 +571,7 @@ def test_successful_authorize_and_capture_with_3ds assert_success response assert_equal 'pay_fj3xswqe3emuxckocjx6td73ni', response.authorization - capture = stub_comms do + capture = stub_comms(@gateway, :ssl_request) do @gateway.capture(@amount, response.authorization) end.respond_with(successful_capture_response) @@ -426,14 +579,15 @@ def test_successful_authorize_and_capture_with_3ds end def test_successful_authorize_and_capture_with_3ds2 - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do options = { execute_threed: true, three_d_secure: { version: '2.0.0', eci: '05', cryptogram: '1234', - ds_transaction_id: '1234' + ds_transaction_id: '1234', + authentication_response_status: 'Y' } } @gateway.authorize(@amount, @credit_card, options) @@ -442,7 +596,7 @@ def test_successful_authorize_and_capture_with_3ds2 assert_success response assert_equal 'pay_fj3xswqe3emuxckocjx6td73ni', response.authorization - capture = stub_comms do + capture = stub_comms(@gateway, :ssl_request) do @gateway.capture(@amount, response.authorization) end.respond_with(successful_capture_response) @@ -450,7 +604,7 @@ def test_successful_authorize_and_capture_with_3ds2 end def test_failed_authorize - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, @credit_card) end.respond_with(failed_authorize_response) @@ -460,7 +614,7 @@ def test_failed_authorize end def test_failed_capture - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.capture(100, '') end.respond_with(failed_capture_response) @@ -468,14 +622,14 @@ def test_failed_capture end def test_successful_void - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, @credit_card) end.respond_with(successful_authorize_response) assert_success response assert_equal 'pay_fj3xswqe3emuxckocjx6td73ni', response.authorization - void = stub_comms do + void = stub_comms(@gateway, :ssl_request) do @gateway.void(response.authorization) end.respond_with(successful_void_response) @@ -483,7 +637,7 @@ def test_successful_void end def test_successful_void_with_metadata - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do options = { metadata: { coupon_code: 'NY2018', @@ -491,7 +645,7 @@ def test_successful_void_with_metadata } } @gateway.authorize(@amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(%r{"coupon_code":"NY2018"}, data) assert_match(%r{"partner_id":"123989"}, data) end.respond_with(successful_authorize_response) @@ -499,7 +653,7 @@ def test_successful_void_with_metadata assert_success response assert_equal 'pay_fj3xswqe3emuxckocjx6td73ni', response.authorization - void = stub_comms do + void = stub_comms(@gateway, :ssl_request) do @gateway.void(response.authorization) end.respond_with(successful_void_response) @@ -507,22 +661,149 @@ def test_successful_void_with_metadata end def test_failed_void - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.void('5d53a33d960c46d00f5dc061947d998c') end.respond_with(failed_void_response) - assert_failure response end + def test_successfully_passes_fund_type_and_fields + options = { + funds_transfer_type: 'FD', + source_type: 'currency_account', + source_id: 'ca_spwmped4qmqenai7hcghquqle4', + account_holder_type: 'individual' + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.credit(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['instruction']['funds_transfer_type'], options[:funds_transfer_type] + assert_equal request['source']['type'], options[:source_type] + assert_equal request['source']['id'], options[:source_id] + assert_equal request['destination']['account_holder']['type'], options[:account_holder_type] + assert_equal request['destination']['account_holder']['first_name'], @credit_card.first_name + assert_equal request['destination']['account_holder']['last_name'], @credit_card.last_name + end.respond_with(successful_credit_response) + assert_success response + end + + def test_successful_money_transfer_payout_via_credit + options = { + instruction_purpose: 'leisure', + account_holder_type: 'individual', + billing_address: address, + payout: true, + destination: { + account_holder: { + phone: { + number: '9108675309', + country_code: '1' + }, + identification: { + type: 'passport', + number: '1234567890' + }, + email: 'too_many_fields@checkout.com', + date_of_birth: '2004-10-27', + country_of_birth: 'US' + } + }, + sender: { + type: 'individual', + first_name: 'Jane', + middle_name: 'Middle', + last_name: 'Doe', + reference: '012345', + reference_type: 'other', + source_of_funds: 'debit', + identification: { + type: 'passport', + number: '0987654321', + issuing_country: 'US', + date_of_expiry: '2027-07-07' + }, + address: { + address1: '205 Main St', + address2: 'Apt G', + city: 'Winchestertonfieldville', + state: 'IA', + country: 'US', + zip: '12345' + }, + date_of_birth: '2004-10-27', + country_of_birth: 'US', + nationality: 'US' + } + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.credit(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['instruction']['purpose'], 'leisure' + assert_equal request['destination']['account_holder']['phone']['number'], '9108675309' + assert_equal request['destination']['account_holder']['phone']['country_code'], '1' + assert_equal request['destination']['account_holder']['identification']['number'], '1234567890' + assert_equal request['destination']['account_holder']['identification']['type'], 'passport' + assert_equal request['destination']['account_holder']['email'], 'too_many_fields@checkout.com' + assert_equal request['destination']['account_holder']['date_of_birth'], '2004-10-27' + assert_equal request['destination']['account_holder']['country_of_birth'], 'US' + assert_equal request['sender']['type'], 'individual' + assert_equal request['sender']['first_name'], 'Jane' + assert_equal request['sender']['middle_name'], 'Middle' + assert_equal request['sender']['last_name'], 'Doe' + assert_equal request['sender']['reference'], '012345' + assert_equal request['sender']['reference_type'], 'other' + assert_equal request['sender']['source_of_funds'], 'debit' + assert_equal request['sender']['identification']['type'], 'passport' + assert_equal request['sender']['identification']['number'], '0987654321' + assert_equal request['sender']['identification']['issuing_country'], 'US' + assert_equal request['sender']['identification']['date_of_expiry'], '2027-07-07' + assert_equal request['sender']['address']['address_line1'], '205 Main St' + assert_equal request['sender']['address']['address_line2'], 'Apt G' + assert_equal request['sender']['address']['city'], 'Winchestertonfieldville' + assert_equal request['sender']['address']['state'], 'IA' + assert_equal request['sender']['address']['country'], 'US' + assert_equal request['sender']['address']['zip'], '12345' + assert_equal request['sender']['date_of_birth'], '2004-10-27' + assert_equal request['sender']['nationality'], 'US' + end.respond_with(successful_credit_response) + assert_success response + end + + def test_transaction_successfully_reverts_to_regular_credit_when_payout_is_nil + options = { + instruction_purpose: 'leisure', + account_holder_type: 'individual', + billing_address: address, + payout: nil, + destination: { + account_holder: { + email: 'too_many_fields@checkout.com' + } + }, + sender: { + type: 'individual' + } + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.credit(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + refute_includes data, 'email' + refute_includes data, 'sender' + end.respond_with(successful_credit_response) + assert_success response + end + def test_successful_refund - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card) end.respond_with(successful_purchase_response) assert_success response assert_equal 'pay_bgv5tmah6fmuzcmcrcro6exe6m', response.authorization - refund = stub_comms do + refund = stub_comms(@gateway, :ssl_request) do @gateway.refund(@amount, response.authorization) end.respond_with(successful_refund_response) @@ -530,7 +811,7 @@ def test_successful_refund end def test_successful_refund_with_metadata - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do options = { metadata: { coupon_code: 'NY2018', @@ -538,7 +819,7 @@ def test_successful_refund_with_metadata } } @gateway.authorize(@amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(%r{"coupon_code":"NY2018"}, data) assert_match(%r{"partner_id":"123989"}, data) end.respond_with(successful_purchase_response) @@ -546,7 +827,7 @@ def test_successful_refund_with_metadata assert_success response assert_equal 'pay_bgv5tmah6fmuzcmcrcro6exe6m', response.authorization - refund = stub_comms do + refund = stub_comms(@gateway, :ssl_request) do @gateway.refund(@amount, response.authorization) end.respond_with(successful_refund_response) @@ -554,7 +835,7 @@ def test_successful_refund_with_metadata end def test_failed_refund - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.refund(nil, '') end.respond_with(failed_refund_response) @@ -562,7 +843,7 @@ def test_failed_refund end def test_successful_verify - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.verify(@credit_card) end.respond_with(successful_verify_response) assert_success response @@ -570,19 +851,50 @@ def test_successful_verify end def test_failed_verify - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.verify(@credit_card) end.respond_with(failed_verify_response) assert_failure response assert_equal 'request_invalid: card_number_invalid', response.message end + def test_successful_store + stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card) + end.check_request do |_method, endpoint, data, _headers| + if /tokens/.match?(endpoint) + assert_match(%r{"type":"card"}, data) + assert_match(%r{"number":"4242424242424242"}, data) + assert_match(%r{"cvv":"123"}, data) + assert_match('/tokens', endpoint) + elsif /instruments/.match?(endpoint) + assert_match(%r{"type":"token"}, data) + assert_match(%r{"token":"tok_}, data) + end + end.respond_with(succesful_token_response, succesful_store_response) + end + + def test_successful_tokenize + stub_comms(@gateway, :ssl_request) do + @gateway.send(:tokenize, @credit_card) + end.check_request do |_action, endpoint, data, _headers| + assert_match(%r{"type":"card"}, data) + assert_match(%r{"number":"4242424242424242"}, data) + assert_match(%r{"cvv":"123"}, data) + assert_match('/tokens', endpoint) + end.respond_with(succesful_token_response) + end + def test_transcript_scrubbing assert_equal post_scrubbed, @gateway.scrub(pre_scrubbed) end + def test_network_transaction_scrubbing + assert_equal network_transaction_post_scrubbed, @gateway.scrub(network_transaction_pre_scrubbed) + end + def test_invalid_json - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card) end.respond_with(invalid_json_response) @@ -591,7 +903,7 @@ def test_invalid_json end def test_error_code_returned - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card) end.respond_with(error_code_response) @@ -600,7 +912,7 @@ def test_error_code_returned end def test_4xx_error_message - @gateway.expects(:ssl_post).raises(error_4xx_response) + @gateway.expects(:ssl_request).raises(error_4xx_response) assert response = @gateway.purchase(@amount, @credit_card) @@ -612,6 +924,58 @@ def test_supported_countries assert_equal %w[AD AE AR AT AU BE BG BH BR CH CL CN CO CY CZ DE DK EE EG ES FI FR GB GR HK HR HU IE IS IT JO JP KW LI LT LU LV MC MT MX MY NL NO NZ OM PE PL PT QA RO SA SE SG SI SK SM TR US], @gateway.supported_countries end + def test_add_shipping_address + options = { + shipping_address: address() + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['shipping']['address']['address_line1'], options[:shipping_address][:address1] + assert_equal request['shipping']['address']['address_line2'], options[:shipping_address][:address2] + assert_equal request['shipping']['address']['city'], options[:shipping_address][:city] + assert_equal request['shipping']['address']['state'], options[:shipping_address][:state] + assert_equal request['shipping']['address']['country'], options[:shipping_address][:country] + assert_equal request['shipping']['address']['zip'], options[:shipping_address][:zip] + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_purchase_supports_alternate_credit_card_implementation + alternate_credit_card_class = Class.new + alternate_credit_card = alternate_credit_card_class.new + + alternate_credit_card.expects(:credit_card?).returns(true) + alternate_credit_card.expects(:name).at_least_once.returns(@credit_card.name) + alternate_credit_card.expects(:number).returns(@credit_card.number) + alternate_credit_card.expects(:verification_value).returns(@credit_card.verification_value) + alternate_credit_card.expects(:first_name).at_least_once.returns(@credit_card.first_name) + alternate_credit_card.expects(:last_name).at_least_once.returns(@credit_card.first_name) + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, alternate_credit_card) + end.respond_with(successful_purchase_response) + end + + def test_authorize_supports_alternate_credit_card_implementation + alternate_credit_card_class = Class.new + alternate_credit_card = alternate_credit_card_class.new + + alternate_credit_card.expects(:credit_card?).returns(true) + alternate_credit_card.expects(:name).at_least_once.returns(@credit_card.name) + alternate_credit_card.expects(:number).returns(@credit_card.number) + alternate_credit_card.expects(:verification_value).returns(@credit_card.verification_value) + alternate_credit_card.expects(:first_name).at_least_once.returns(@credit_card.first_name) + alternate_credit_card.expects(:last_name).at_least_once.returns(@credit_card.first_name) + + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, alternate_credit_card) + end.respond_with(successful_authorize_response) + end + private def pre_scrubbed @@ -621,6 +985,20 @@ def pre_scrubbed ) end + def network_transaction_pre_scrubbed + %q( + <- "POST /payments HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\nAuthorization: sk_test_ab12301d-e432-4ea7-97d1-569809518aaf\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api.checkout.com\r\nContent-Length: 346\r\n\r\n" + <- "{\"amount\":\"100\",\"reference\":\"1\",\"currency\":\"USD\",\"metadata\":{\"udf5\":\"ActiveMerchant\"},\"source\":{\"type\":\"network_token\",\"token\":\"4242424242424242\",\"token_type\":\"applepay\",\"cryptogram\":\"AgAAAAAAAIR8CQrXcIhbQAAAAAA\",\"eci\":\"05\",\"expiry_year\":\"2025\",\"expiry_month\":\"10\",\"billing_address\":{\"address_line1\":\"456 My Street\",\"address_line2\":\"Apt 1\",\"city\":\"Ottawa\",\"state\":\"ON\",\"country\":\"CA\",\"zip\":\"K1C2N6\"}},\"customer\":{\"email\":\"longbob.longsen@example.com\"}}" + ) + end + + def network_transaction_post_scrubbed + %q( + <- "POST /payments HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\nAuthorization: [FILTERED]\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api.checkout.com\r\nContent-Length: 346\r\n\r\n" + <- "{\"amount\":\"100\",\"reference\":\"1\",\"currency\":\"USD\",\"metadata\":{\"udf5\":\"ActiveMerchant\"},\"source\":{\"type\":\"network_token\",\"token\":\"[FILTERED]\",\"token_type\":\"applepay\",\"cryptogram\":\"[FILTERED]\",\"eci\":\"05\",\"expiry_year\":\"2025\",\"expiry_month\":\"10\",\"billing_address\":{\"address_line1\":\"456 My Street\",\"address_line2\":\"Apt 1\",\"city\":\"Ottawa\",\"state\":\"ON\",\"country\":\"CA\",\"zip\":\"K1C2N6\"}},\"customer\":{\"email\":\"longbob.longsen@example.com\"}}" + ) + end + def post_scrubbed %q( <- "POST /payments HTTP/1.1\r\nContent-Type: application/json;charset=UTF-8\r\nAuthorization: [FILTERED]\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: api.checkout.com\r\nContent-Length: 346\r\n\r\n" @@ -634,6 +1012,12 @@ def successful_purchase_response ) end + def succesful_store_response + %( + {"id":"src_vzzqipykt5ke5odazx5d7nikii","type":"card","fingerprint":"9F3BAD2E48C6C8579F2F5DC0710B7C11A8ACD5072C3363A72579A6FB227D64BE","expiry_month":6,"expiry_year":2025,"scheme":"VISA","last4":"4242","bin":"424242","card_type":"CREDIT","card_category":"CONSUMER","issuer_country":"GB","product_id":"F","product_type":"Visa Classic","customer":{"id":"cus_gmthnluatgounpoiyzbmn5fvua", "email":"longbob.longsen@example.com"}} + ) + end + def successful_purchase_with_network_token_response purchase_response = JSON.parse(successful_purchase_response) purchase_response['source']['payment_account_reference'] = '2FCFE326D92D4C27EDD699560F484' @@ -753,6 +1137,56 @@ def failed_authorize_response ) end + def successful_incremental_authorize_response + %( + { + "action_id": "act_q4dbxom5jbgudnjzjpz7j2z6uq", + "amount": 50, + "currency": "USD", + "approved": true, + "status": "Authorized", + "auth_code": "503198", + "expires_on": "2020-04-20T10:11:12Z", + "eci": "05", + "scheme_id": "511129554406717", + "response_code": "10000", + "response_summary": "Approved", + "balances": { + "total_authorized": 150, + "total_voided": 0, + "available_to_void": 150, + "total_captured": 0, + "available_to_capture": 150, + "total_refunded": 0, + "available_to_refund": 0 + }, + "processed_on": "2020-03-16T22:11:24Z", + "reference": "ORD-752-814", + "processing": { + "acquirer_transaction_id": "8367314942", + "retrieval_reference_number": "162588399162" + }, + "_links": { + "self": { + "href": "https://api.sandbox.checkout.com/payments/pay_tqgk5c6k2nnexagtcuom5ktlua" + }, + "actions": { + "href": "https://api.sandbox.checkout.com/payments/pay_tqgk5c6k2nnexagtcuom5ktlua/actions" + }, + "authorize": { + "href": "https://api.sandbox.checkout.com/payments/pay_tqgk5c6k2nnexagtcuom5ktlua/authorizations" + }, + "capture": { + "href": "https://api.sandbox.checkout.com/payments/pay_tqgk5c6k2nnexagtcuom5ktlua/captures" + }, + "void": { + "href": "https://api.sandbox.checkout.com/payments/pay_tqgk5c6k2nnexagtcuom5ktlua/voids" + } + } + } + ) + end + def successful_capture_response %( { @@ -790,6 +1224,27 @@ def successful_void_response ) end + def successful_credit_response + %( + { + "id": "pay_jhzh3u7vxcgezlcek7ymzyy6be", + "status": "Pending", + "reference": "ORD-5023-4E89", + "instruction": { + "value_date": "2022-08-09T06:11:37.2306547+00:00" + }, + "_links": { + "self": { + "href": "https://api.sandbox.checkout.com/payments/pay_jhzh3u7vxcgezlcek7ymzyy6be" + }, + "actions": { + "href": "https://api.sandbox.checkout.com/payments/pay_jhzh3u7vxcgezlcek7ymzyy6be/actions" + } + } + } + ) + end + def failed_void_response %( ) @@ -823,6 +1278,10 @@ def successful_verify_payment_response ) end + def succesful_token_response + %({"type":"card","token":"tok_267wy4hwrpietkmbbp5iswwhvm","expires_on":"2023-01-03T20:18:49.0006481Z","expiry_month":6,"expiry_year":2025,"name":"Longbob Longsen","scheme":"VISA","last4":"4242","bin":"424242","card_type":"CREDIT","card_category":"CONSUMER","issuer_country":"GB","product_id":"F","product_type":"Visa Classic"}) + end + def failed_verify_payment_response %( {"id":"pay_xrwmaqlar73uhjtyoghc7bspa4","requested_on":"2019-08-14T18:32:50Z","source":{"type":"card","expiry_month":12,"expiry_year":2020,"name":"Jane Doe","scheme":"Visa","last4":"7863","fingerprint":"DC20145B78E242C561A892B83CB64471729D7A5063E5A5B341035713B8FDEC92","bin":"453962"},"amount":100,"currency":"USD","payment_type":"Regular","reference":"EuyOZtgt8KI4tolEH8lqxCclWqz","status":"Declined","approved":false,"3ds":{"downgraded":false,"enrolled":"Y","version":"2.1.0"},"risk":{"flagged":false},"customer":{"id":"cus_bb4b7eu35sde7o33fq2xchv7oq","name":"Jane Doe"},"payment_ip":"127.0.0.1","metadata":{"Udf5":"ActiveMerchant"},"_links":{"self":{"href":"https://api.sandbox.checkout.com/payments/pay_xrwmaqlar73uhjtyoghc7bspa4"},"actions":{"href":"https://api.sandbox.checkout.com/payments/pay_xrwmaqlar73uhjtyoghc7bspa4/actions"}}} diff --git a/test/unit/gateways/citrus_pay_test.rb b/test/unit/gateways/citrus_pay_test.rb index 7c96c1c1308..bc39c2034bd 100644 --- a/test/unit/gateways/citrus_pay_test.rb +++ b/test/unit/gateways/citrus_pay_test.rb @@ -157,7 +157,7 @@ def test_unsuccessful_verify assert_equal 'FAILURE - DECLINED', response.message end - def test_north_america_region_url + def test_url @gateway = TnsGateway.new( userid: 'userid', password: 'password', @@ -167,23 +167,7 @@ def test_north_america_region_url response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, @options) end.check_request do |_method, endpoint, _data, _headers| - assert_match(/secure.na.tnspayments.com/, endpoint) - end.respond_with(successful_capture_response) - - assert_success response - end - - def test_asia_pacific_region_url - @gateway = TnsGateway.new( - userid: 'userid', - password: 'password', - region: 'asia_pacific' - ) - - response = stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |_method, endpoint, _data, _headers| - assert_match(/secure.ap.tnspayments.com/, endpoint) + assert_match(/secure.uat.tnspayments.com/, endpoint) end.respond_with(successful_capture_response) assert_success response diff --git a/test/unit/gateways/commerce_hub_test.rb b/test/unit/gateways/commerce_hub_test.rb new file mode 100644 index 00000000000..b2b085be356 --- /dev/null +++ b/test/unit/gateways/commerce_hub_test.rb @@ -0,0 +1,806 @@ +require 'test_helper' + +class CommerceHubTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = CommerceHubGateway.new(api_key: 'login', api_secret: 'password', merchant_id: '12345', terminal_id: '0001') + + @amount = 1204 + @credit_card = credit_card('4005550000000019', month: '02', year: '2035', verification_value: '123') + @google_pay = network_tokenization_credit_card( + '4005550000000019', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :google_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + transaction_id: '13456789' + ) + @apple_pay = network_tokenization_credit_card( + '4005550000000019', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + transaction_id: '13456789' + ) + @no_supported_source = network_tokenization_credit_card( + '4005550000000019', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :no_source, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @declined_card = credit_card('4000300011112220', month: '02', year: '2035', verification_value: '123') + @options = {} + @post = {} + end + + def test_successful_authorize_with_full_headers + @options.merge!( + headers_identifiers: { + 'x-originator' => 'CommerceHub-Partners-Spreedly', + 'user-agent' => 'CommerceHub-Partners-Spreedly-V1.00' + } + ) + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, _data, headers| + assert_not_nil headers['Client-Request-Id'] + assert_equal 'login', headers['Api-Key'] + assert_not_nil headers['Timestamp'] + assert_equal 'application/json', headers['Accept-Language'] + assert_equal 'application/json', headers['Content-Type'] + assert_equal 'application/json', headers['Accept'] + assert_equal 'HMAC', headers['Auth-Token-Type'] + assert_not_nil headers['Authorization'] + assert_equal 'CommerceHub-Partners-Spreedly', headers['x-originator'] + assert_equal 'CommerceHub-Partners-Spreedly-V1.00', headers['user-agent'] + end.respond_with(successful_authorize_response) + end + + def test_successful_purchase + @options[:order_id] = 'abc123' + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionDetails']['captureFlag'], true + assert_equal request['transactionDetails']['createToken'], false + assert_equal request['transactionDetails']['merchantOrderId'], 'abc123' + assert_equal request['merchantDetails']['terminalId'], @gateway.options[:terminal_id] + assert_equal request['merchantDetails']['merchantId'], @gateway.options[:merchant_id] + assert_equal request['amount']['total'], (@amount / 100.0).to_f + assert_equal request['source']['card']['cardData'], @credit_card.number + assert_equal request['source']['card']['securityCode'], @credit_card.verification_value + assert_equal request['transactionInteraction']['posEntryMode'], 'MANUAL' + assert_equal request['source']['card']['securityCodeIndicator'], 'PROVIDED' + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_google_pay + response = stub_comms do + @gateway.purchase(@amount, @google_pay, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionDetails']['captureFlag'], true + assert_equal request['merchantDetails']['terminalId'], @gateway.options[:terminal_id] + assert_equal request['merchantDetails']['merchantId'], @gateway.options[:merchant_id] + assert_equal request['amount']['total'], (@amount / 100.0).to_f + assert_equal request['source']['card']['cardData'], @google_pay.number + assert_equal request['source']['cavv'], @google_pay.payment_cryptogram + assert_equal request['source']['walletType'], 'GOOGLE_PAY' + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_apple_pay + response = stub_comms do + @gateway.purchase(@amount, @apple_pay, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionDetails']['captureFlag'], true + assert_equal request['merchantDetails']['terminalId'], @gateway.options[:terminal_id] + assert_equal request['merchantDetails']['merchantId'], @gateway.options[:merchant_id] + assert_equal request['amount']['total'], (@amount / 100.0).to_f + assert_equal request['source']['card']['cardData'], @apple_pay.number + assert_equal request['source']['cavv'], @apple_pay.payment_cryptogram + assert_equal request['source']['walletType'], 'APPLE_PAY' + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_no_supported_source_as_apple_pay + response = stub_comms do + @gateway.purchase(@amount, @no_supported_source, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionDetails']['captureFlag'], true + assert_equal request['merchantDetails']['terminalId'], @gateway.options[:terminal_id] + assert_equal request['merchantDetails']['merchantId'], @gateway.options[:merchant_id] + assert_equal request['amount']['total'], (@amount / 100.0).to_f + assert_equal request['source']['card']['cardData'], @apple_pay.number + assert_equal request['source']['cavv'], @apple_pay.payment_cryptogram + assert_equal request['source']['walletType'], 'APPLE_PAY' + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_authorize + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionDetails']['captureFlag'], false + assert_equal request['merchantDetails']['terminalId'], @gateway.options[:terminal_id] + assert_equal request['merchantDetails']['merchantId'], @gateway.options[:merchant_id] + assert_equal request['amount']['total'], (@amount / 100.0).to_f + assert_equal request['source']['card']['cardData'], @credit_card.number + assert_equal request['source']['card']['securityCode'], @credit_card.verification_value + assert_equal request['source']['card']['securityCodeIndicator'], 'PROVIDED' + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_failed_purchase_and_authorize + @gateway.expects(:ssl_post).returns(failed_purchase_and_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal 'string', response.error_code + end + + def test_successful_parsing_of_billing_and_shipping_addresses + address_with_phone = address.merge({ phone_number: '000-000-00-000' }) + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ billing_address: address_with_phone, shipping_address: address_with_phone })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + %w(shipping billing).each do |key| + assert_equal request[key + 'Address']['address']['street'], address_with_phone[:address1] + assert_equal request[key + 'Address']['address']['houseNumberOrName'], address_with_phone[:address2] + assert_equal request[key + 'Address']['address']['recipientNameOrAddress'], address_with_phone[:name] + assert_equal request[key + 'Address']['address']['city'], address_with_phone[:city] + assert_equal request[key + 'Address']['address']['stateOrProvince'], address_with_phone[:state] + assert_equal request[key + 'Address']['address']['postalCode'], address_with_phone[:zip] + assert_equal request[key + 'Address']['address']['country'], address_with_phone[:country] + assert_equal request[key + 'Address']['phone']['phoneNumber'], address_with_phone[:phone_number] + end + end.respond_with(successful_authorize_response) + end + + def test_successful_void + response = stub_comms do + @gateway.void('abc123|authorization123', @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal 'authorization123', request['referenceTransactionDetails']['referenceTransactionId'] + assert_equal 'CHARGES', request['referenceTransactionDetails']['referenceTransactionType'] + assert_nil request['transactionDetails']['captureFlag'] + end.respond_with(successful_void_and_refund_response) + + assert_success response + end + + def test_successful_refund + response = stub_comms do + @gateway.refund(nil, 'abc123|authorization123', @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['referenceTransactionDetails']['referenceTransactionId'], 'authorization123' + assert_equal request['referenceTransactionDetails']['referenceTransactionType'], 'CHARGES' + assert_nil request['transactionDetails']['captureFlag'] + assert_nil request['amount'] + end.respond_with(successful_void_and_refund_response) + + assert_success response + end + + def test_successful_partial_refund + response = stub_comms do + @gateway.refund(@amount - 1, 'abc123|authorization123', @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['referenceTransactionDetails']['referenceTransactionId'], 'authorization123' + assert_equal request['referenceTransactionDetails']['referenceTransactionType'], 'CHARGES' + assert_nil request['transactionDetails']['captureFlag'] + assert_equal request['amount']['total'], ((@amount - 1) / 100.0).to_f + assert_equal request['amount']['currency'], 'USD' + end.respond_with(successful_void_and_refund_response) + + assert_success response + end + + def test_successful_purchase_cit_with_gsf + options = stored_credential_options(:cardholder, :unscheduled, :initial) + options[:data_entry_source] = 'MOBILE_WEB' + options[:pos_entry_mode] = 'MANUAL' + options[:pos_condition_code] = 'CARD_PRESENT' + response = stub_comms do + @gateway.purchase(@amount, 'authorization123', options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionInteraction']['origin'], 'ECOM' + assert_equal request['transactionInteraction']['eciIndicator'], 'CHANNEL_ENCRYPTED' + assert_equal request['transactionInteraction']['posConditionCode'], 'CARD_PRESENT' + assert_equal request['transactionInteraction']['posEntryMode'], 'MANUAL' + assert_equal request['transactionInteraction']['additionalPosInformation']['dataEntrySource'], 'MOBILE_WEB' + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_successful_purchase_mit_with_gsf + options = stored_credential_options(:merchant, :recurring) + options[:origin] = 'POS' + options[:pos_entry_mode] = 'MANUAL' + options[:data_entry_source] = 'MOBILE_WEB' + response = stub_comms do + @gateway.purchase(@amount, 'authorization123', options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionInteraction']['origin'], 'POS' + assert_equal request['transactionInteraction']['eciIndicator'], 'CHANNEL_ENCRYPTED' + assert_equal request['transactionInteraction']['posConditionCode'], 'CARD_NOT_PRESENT_ECOM' + assert_equal request['transactionInteraction']['posEntryMode'], 'MANUAL' + assert_equal request['transactionInteraction']['additionalPosInformation']['dataEntrySource'], 'MOBILE_WEB' + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_successful_purchase_with_gsf_scheme_reference_transaction_id + @options = stored_credential_options(:cardholder, :unscheduled, :initial) + @options[:physical_goods_indicator] = true + @options[:scheme_reference_transaction_id] = '12345' + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['storedCredentials']['schemeReferenceTransactionId'], '12345' + assert_equal request['transactionDetails']['physicalGoodsIndicator'], true + end.respond_with(successful_purchase_response) + + assert_success response + end + + def stored_credential_options(*args, ntid: nil) + { + order_id: '#1001', + stored_credential: stored_credential(*args, ntid: ntid) + } + end + + def test_successful_store + response = stub_comms do + @gateway.store(@credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['merchantDetails']['terminalId'], @gateway.options[:terminal_id] + assert_equal request['merchantDetails']['merchantId'], @gateway.options[:merchant_id] + assert_equal request['source']['card']['cardData'], @credit_card.number + assert_equal request['source']['card']['securityCode'], @credit_card.verification_value + assert_equal request['source']['card']['securityCodeIndicator'], 'PROVIDED' + end.respond_with(successful_store_response) + + assert_success response + assert_equal response.params['paymentTokens'].first['tokenData'], response.authorization + end + + def test_successful_verify + stub_comms do + @gateway.verify(@credit_card, @options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_match %r{verification}, endpoint + assert_equal request['source']['sourceType'], 'PaymentCard' + end.respond_with(successful_authorize_response) + end + + def test_getting_avs_cvv_from_response + gateway_resp = { + 'paymentReceipt' => { + 'processorResponseDetails' => { + 'bankAssociationDetails' => { + 'associationResponseCode' => 'V000', + 'avsSecurityCodeResponse' => { + 'streetMatch' => 'NONE', + 'postalCodeMatch' => 'NONE', + 'securityCodeMatch' => 'NOT_CHECKED', + 'association' => { + 'securityCodeResponse' => 'X', + 'avsCode' => 'Y' + } + } + } + } + } + } + + assert_equal 'X', @gateway.send(:get_avs_cvv, gateway_resp, 'cvv') + assert_equal 'Y', @gateway.send(:get_avs_cvv, gateway_resp, 'avs') + end + + def test_successful_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_uses_order_id_to_keep_transaction_references_when_provided + @options[:order_id] = 'abc123' + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'abc123|6304d53be8d94312a620962afc9c012d', response.authorization + end + + def test_detect_success_state_for_verify_on_success_transaction + gateway_resp = { + 'gatewayResponse' => { + 'transactionState' => 'VERIFIED' + } + } + + assert @gateway.send :success_from, gateway_resp, 'verify' + end + + def test_detect_success_state_for_verify_on_failure_transaction + gateway_resp = { + 'gatewayResponse' => { + 'transactionState' => 'NOT_VERIFIED' + } + } + + refute @gateway.send :success_from, gateway_resp, 'verify' + end + + def test_add_reference_transaction_details_capture_reference_id + authorization = '|922e-59fc86a36c03' + + @gateway.send :add_reference_transaction_details, @post, authorization, {}, :capture + assert_equal '922e-59fc86a36c03', @post[:referenceTransactionDetails][:referenceTransactionId] + assert_nil @post[:referenceTransactionDetails][:referenceTransactionType] + end + + def test_add_reference_transaction_details_void_reference_id + authorization = '|922e-59fc86a36c03' + + @gateway.send :add_reference_transaction_details, @post, authorization, {}, :void + assert_equal '922e-59fc86a36c03', @post[:referenceTransactionDetails][:referenceTransactionId] + assert_equal 'CHARGES', @post[:referenceTransactionDetails][:referenceTransactionType] + end + + def test_add_reference_transaction_details_refund_reference_id + authorization = '|922e-59fc86a36c03' + + @gateway.send :add_reference_transaction_details, @post, authorization, {}, :refund + assert_equal '922e-59fc86a36c03', @post[:referenceTransactionDetails][:referenceTransactionId] + assert_equal 'CHARGES', @post[:referenceTransactionDetails][:referenceTransactionType] + end + + def test_successful_purchase_when_encrypted_credit_card_present + @options[:order_id] = 'abc123' + @options[:encryption_data] = { + keyId: SecureRandom.uuid, + encryptionType: 'RSA', + encryptionBlock: SecureRandom.alphanumeric(20), + encryptionBlockFields: 'card.cardData:16,card.nameOnCard:8,card.expirationMonth:2,card.expirationYear:4,card.securityCode:3', + encryptionTarget: 'MANUAL' + } + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + refute_nil request['source']['encryptionData'] + assert_equal request['source']['sourceType'], 'PaymentCard' + assert_equal request['source']['encryptionData']['keyId'], @options[:encryption_data][:keyId] + assert_equal request['source']['encryptionData']['encryptionType'], 'RSA' + assert_equal request['source']['encryptionData']['encryptionBlock'], @options[:encryption_data][:encryptionBlock] + assert_equal request['source']['encryptionData']['encryptionBlockFields'], @options[:encryption_data][:encryptionBlockFields] + assert_equal request['source']['encryptionData']['encryptionTarget'], 'MANUAL' + end.respond_with(successful_purchase_response) + + assert_success response + end + + private + + def successful_purchase_response + <<~RESPONSE + { + "gatewayResponse": { + "transactionType": "CHARGE", + "transactionState": "CAPTURED", + "transactionOrigin": "ECOM", + "transactionProcessingDetails": { + "orderId": "CHG018048a66aafc64d789cb018a53c30fd74", + "transactionTimestamp": "2022-10-06T11:27:45.593359Z", + "apiTraceId": "6304d53be8d94312a620962afc9c012d", + "clientRequestId": "5106241", + "transactionId": "6304d53be8d94312a620962afc9c012d" + } + }, + "source": { + "sourceType": "PaymentCard", + "card": { + "expirationMonth": "02", + "expirationYear": "2035", + "securityCodeIndicator": "PROVIDED", + "bin": "400555", + "last4": "0019", + "scheme": "VISA" + } + }, + "paymentReceipt": { + "approvedAmount": { + "total": 12.04, + "currency": "USD" + }, + "processorResponseDetails": { + "approvalStatus": "APPROVED", + "approvalCode": "000238", + "referenceNumber": "962afc9c012d", + "processor": "FISERV", + "host": "NASHVILLE", + "networkInternationalId": "0001", + "responseCode": "000", + "responseMessage": "Approved", + "hostResponseCode": "00", + "additionalInfo": [ + { + "name": "HOST_RAW_PROCESSOR_RESPONSE", + "value": "ARAyIAHvv70O77+9AAAAAAAAAAAAEgQQBhAnRQE1JAABWTk2MmFmYzljMDEyZDAwMDIzODAwMDk5OTk5OTk=" + } + ] + } + }, + "transactionDetails": { + "captureFlag": true, + "transactionCaptureType": "hcs", + "processingCode": "000000", + "merchantInvoiceNumber": "123456789012", + "createToken": true, + "retrievalReferenceNumber": "962afc9c012d" + }, + "transactionInteraction": { + "posEntryMode": "UNSPECIFIED", + "posConditionCode": "CARD_NOT_PRESENT_ECOM", + "additionalPosInformation": { + "dataEntrySource": "UNSPECIFIED", + "posFeatures": { + "pinAuthenticationCapability": "UNSPECIFIED", + "terminalEntryCapability": "UNSPECIFIED" + } + }, + "hostPosConditionCode": "59" + }, + "merchantDetails": { + "tokenType": "BBY0", + "terminalId": "10000001", + "merchantId": "100008000003683" + }, + "networkDetails": { + "network": { + "network": "Visa" + } + } + } + RESPONSE + end + + def successful_authorize_response + <<~RESPONSE + { + "gatewayResponse": { + "transactionType": "CHARGE", + "transactionState": "AUTHORIZED", + "transactionOrigin": "ECOM", + "transactionProcessingDetails": { + "orderId": "CHG01fb29348b9f8a48ed875e6bea3af41744", + "transactionTimestamp": "2022-10-06T11:28:27.131701Z", + "apiTraceId": "000bc22420f448288f1226d28dfdf275", + "clientRequestId": "9573527", + "transactionId": "000bc22420f448288f1226d28dfdf275" + } + }, + "source": { + "sourceType": "PaymentCard", + "card": { + "expirationMonth": "02", + "expirationYear": "2035", + "bin": "400555", + "last4": "0019", + "scheme": "VISA" + } + }, + "paymentReceipt": { + "approvedAmount": { + "total": 12.04, + "currency": "USD" + }, + "processorResponseDetails": { + "approvalStatus": "APPROVED", + "approvalCode": "000239", + "referenceNumber": "26d28dfdf275", + "processor": "FISERV", + "host": "NASHVILLE", + "networkInternationalId": "0001", + "responseCode": "000", + "responseMessage": "Approved", + "hostResponseCode": "00", + "additionalInfo": [ + { + "name": "HOST_RAW_PROCESSOR_RESPONSE", + "value": "ARAyIAHvv70O77+9AAAAAAAAAAAAEgQQBhAoJzQ2aAABWTI2ZDI4ZGZkZjI3NTAwMDIzOTAwMDk5OTk5OTk=" + } + ] + } + }, + "transactionDetails": { + "captureFlag": false, + "transactionCaptureType": "hcs", + "processingCode": "000000", + "merchantInvoiceNumber": "123456789012", + "createToken": true, + "retrievalReferenceNumber": "26d28dfdf275" + }, + "transactionInteraction": { + "posEntryMode": "UNSPECIFIED", + "posConditionCode": "CARD_NOT_PRESENT_ECOM", + "additionalPosInformation": { + "dataEntrySource": "UNSPECIFIED", + "posFeatures": { + "pinAuthenticationCapability": "UNSPECIFIED", + "terminalEntryCapability": "UNSPECIFIED" + } + }, + "hostPosConditionCode": "59" + }, + "merchantDetails": { + "tokenType": "BBY0", + "terminalId": "10000001", + "merchantId": "100008000003683" + }, + "networkDetails": { + "network": { + "network": "Visa" + } + } + } + RESPONSE + end + + def failed_purchase_and_authorize_response + <<~RESPONSE + { + "gatewayResponse": { + "transactionType": "CHARGE", + "transactionState": "AUTHORIZED", + "transactionOrigin": "ECOM", + "transactionProcessingDetails": { + "orderId": "R-3b83fca8-2f9c-4364-86ae-12c91f1fcf16", + "transactionTimestamp": "2016-04-16T16:06:05Z", + "apiTraceId": "1234567a1234567b1234567c1234567d", + "clientRequestId": "30dd879c-ee2f-11db-8314-0800200c9a66", + "transactionId": "838916029301" + } + }, + "error": [ + { + "type": "HOST", + "code": "string", + "field": "source.sourceType", + "message": "Missing type ID property.", + "additionalInfo": "The Reauthorization request was not successful and the Cancel of referenced authorization transaction was not processed, per Auth_before_Cancel configuration" + } + ] + } + RESPONSE + end + + def successful_void_and_refund_response + <<~RESPONSE + { + "gatewayResponse": { + "transactionType": "CANCEL", + "transactionState": "AUTHORIZED", + "transactionOrigin": "ECOM", + "transactionProcessingDetails": { + "transactionTimestamp": "2021-06-20T23:42:48Z", + "orderId": "RKOrdID-525133851837", + "apiTraceId": "362866ac81864d7c9d1ff8b5aa6e98db", + "clientRequestId": "4345791", + "transactionId": "84356531338" + } + }, + "source": { + "sourceType": "PaymentCard", + "card": { + "bin": "40055500", + "last4": "0019", + "scheme": "VISA", + "expirationMonth": "10", + "expirationYear": "2030" + } + }, + "paymentReceipt": { + "approvedAmount": { + "total": 12.04, + "currency": "USD" + }, + "merchantName": "Merchant Name", + "merchantAddress": "123 Peach Ave", + "merchantCity": "Atlanta", + "merchantStateOrProvince": "GA", + "merchantPostalCode": "12345", + "merchantCountry": "US", + "merchantURL": "https://www.somedomain.com", + "processorResponseDetails": { + "approvalStatus": "APPROVED", + "approvalCode": "OK5882", + "schemeTransactionId": "0225MCC625628", + "processor": "FISERV", + "host": "NASHVILLE", + "responseCode": "000", + "responseMessage": "APPROVAL", + "hostResponseCode": "00", + "hostResponseMessage": "APPROVAL", + "localTimestamp": "2021-06-20T23:42:48Z", + "bankAssociationDetails": { + "associationResponseCode": "000", + "transactionTimestamp": "2021-06-20T23:42:48Z" + } + } + }, + "transactionDetails": { + "merchantInvoiceNumber": "123456789012" + } + } + RESPONSE + end + + def successful_store_response + <<~RESPONSE + { + "gatewayResponse": { + "transactionType": "TOKENIZE", + "transactionState": "AUTHORIZED", + "transactionOrigin": "ECOM", + "transactionProcessingDetails": { + "transactionTimestamp": "2021-06-20T23:42:48Z", + "orderId": "RKOrdID-525133851837", + "apiTraceId": "362866ac81864d7c9d1ff8b5aa6e98db", + "clientRequestId": "4345791", + "transactionId": "84356531338" + } + }, + "source": { + "sourceType": "PaymentCard", + "card": { + "bin": "40055500", + "last4": "0019", + "scheme": "VISA", + "expirationMonth": "10", + "expirationYear": "2030" + } + }, + "paymentTokens": [ + { + "tokenData": "8519371934460009", + "tokenSource": "TRANSARMOR", + "tokenResponseCode": "000", + "tokenResponseDescription": "SUCCESS" + }, + { + "tokenData": "8519371934460010", + "tokenSource": "CHASE", + "tokenResponseCode": "000", + "tokenResponseDescription": "SUCCESS" + } + ], + "processorResponseDetails": { + "approvalStatus": "APPROVED", + "approvalCode": "OK5882", + "schemeTransactionId": "0225MCC625628", + "processor": "FISERV", + "host": "NASHVILLE", + "responseCode": "000", + "responseMessage": "APPROVAL", + "hostResponseCode": "00", + "hostResponseMessage": "APPROVAL", + "localTimestamp": "2021-06-20T23:42:48Z", + "bankAssociationDetails": { + "associationResponseCode": "000", + "transactionTimestamp": "2021-06-20T23:42:48Z" + } + } + } + RESPONSE + end + + def pre_scrubbed + <<~PRE_SCRUBBED + opening connection to cert.api.fiservapps.com:443... + opened + starting SSL for cert.api.fiservapps.com:443... + SSL established + <- "POST /ch/payments/v1/charges HTTP/1.1\r\nContent-Type: application/json\r\nClient-Request-Id: 3473900\r\nApi-Key: nEcoHEQZjKtkKW9dN6yH7x4gO2EIARKe\r\nTimestamp: 1670258885014\r\nAccept-Language: application/json\r\nAuth-Token-Type: HMAC\r\nAccept: application/json\r\nAuthorization: TQh0nE38Mv7cbxbX3oSIUxZ4RzMkTmS2hpUSd6Rgi98=\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: cert.api.fiservapps.com\r\nContent-Length: 500\r\n\r\n" + <- "{\"transactionDetails\":{\"captureFlag\":true,\"merchantInvoiceNumber\":\"995952121195\"},\"amount\":{\"total\":12.04,\"currency\":\"USD\"},\"source\":{\"sourceType\":\"PaymentCard\",\"card\":{\"cardData\":\"4005550000000019\",\"expirationMonth\":\"02\",\"expirationYear\":\"2035\",\"securityCode\":\"123\",\"securityCodeIndicator\":\"PROVIDED\"}},\"transactionInteraction\":{\"origin\":\"ECOM\",\"eciIndicator\":\"CHANNEL_ENCRYPTED\",\"posConditionCode\":\"CARD_NOT_PRESENT_ECOM\"},\"merchantDetails\":{\"terminalId\":\"10000001\",\"merchantId\":\"100008000003683\"}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Mon, 05 Dec 2022 16:48:06 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 1709\r\n" + -> "Connection: close\r\n" + -> "Expires: 0\r\n" + -> "Referrer-Policy: no-referrer\r\n" + -> "X-Frame-Options: DENY\r\n" + -> "X-Vcap-Request-Id: 30397096-5cb9-46e1-7c63-3ac2494ca38e\r\n" + -> "targetServerReceivedEndTimestamp: 1670258886388\r\n" + -> "targetServerSentStartTimestamp: 1670258885212\r\n" + -> "X-Xss-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "Cache-Control: no-store, no-cache, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "Access-Control-Max-Age: 86400\r\n" + -> "ApiTraceId: 19d178570f274a2196540af6e2e0bf55\r\n" + -> "Via: 1.1 dca1-bit16021\r\n" + -> "\r\n" + reading 1709 bytes... + -> "{\"gatewayResponse\":{\"transactionType\":\"CHARGE\",\"transactionState\":\"CAPTURED\",\"transactionOrigin\":\"ECOM\",\"transactionProcessingDetails\":{\"orderId\":\"CHG0147086beb95194e808a3bf88e052285d7\",\"transactionTimestamp\":\"2022-12-05T16:48:05.358725Z\",\"apiTraceId\":\"19d178570f274a2196540af6e2e0bf55\",\"clientRequestId\":\"3473900\",\"transactionId\":\"19d178570f274a2196540af6e2e0bf55\"}},\"source\":{\"sourceType\":\"PaymentCard\",\"card\":{\"expirationMonth\":\"02\",\"expirationYear\":\"2035\",\"securityCodeIndicator\":\"PROVIDED\",\"bin\":\"400555\",\"last4\":\"0019\",\"scheme\":\"VISA\"}},\"paymentReceipt\":{\"approvedAmount\":{\"total\":12.04,\"currency\":\"USD\"},\"processorResponseDetails\":{\"approvalStatus\":\"APPROVED\",\"approvalCode\":\"000119\",\"referenceNumber\":\"0af6e2e0bf55\",\"processor\":\"FISERV\",\"host\":\"NASHVILLE\",\"networkInternationalId\":\"0001\",\"responseCode\":\"000\",\"responseMessage\":\"Approved\",\"hostResponseCode\":\"00\",\"additionalInfo\":[{\"name\":\"HOST_RAW_PROCESSOR_RESPONSE\",\"value\":\"ARAyIAHvv70O77+9AAIAAAAAAAAAEgQSBRZIBTNCVQABWTBhZjZlMmUwYmY1NTAwMDExOTAwMDk5OTk5OTkABAACMTQ=\"}]}},\"transactionDetails\":{\"captureFlag\":true,\"transactionCaptureType\":\"hcs\",\"processingCode\":\"000000\",\"merchantInvoiceNumber\":\"995952121195\",\"createToken\":true,\"retrievalReferenceNumber\":\"0af6e2e0bf55\",\"cavvInPrimary\":false},\"transactionInteraction\":{\"posEntryMode\":\"UNSPECIFIED\",\"posConditionCode\":\"CARD_NOT_PRESENT_ECOM\",\"additionalPosInformation\":{\"dataEntrySource\":\"UNSPECIFIED\",\"posFeatures\":{\"pinAuthenticationCapability\":\"UNSPECIFIED\",\"terminalEntryCapability\":\"UNSPECIFIED\"}},\"hostPosEntryMode\":\"000\",\"hostPosConditionCode\":\"59\"},\"merchantDetails\":{\"tokenType\":\"BBY0\",\"terminalId\":\"10000001\",\"merchantId\":\"100008000003683\"},\"networkDetails\":{\"network\":{\"network\":\"Visa\"}}}" + read 1709 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<~POST_SCRUBBED + opening connection to cert.api.fiservapps.com:443... + opened + starting SSL for cert.api.fiservapps.com:443... + SSL established + <- "POST /ch/payments/v1/charges HTTP/1.1\r\nContent-Type: application/json\r\nClient-Request-Id: 3473900\r\nApi-Key: [FILTERED]\r\nTimestamp: 1670258885014\r\nAccept-Language: application/json\r\nAuth-Token-Type: HMAC\r\nAccept: application/json\r\nAuthorization: [FILTERED]\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: cert.api.fiservapps.com\r\nContent-Length: 500\r\n\r\n" + <- "{\"transactionDetails\":{\"captureFlag\":true,\"merchantInvoiceNumber\":\"995952121195\"},\"amount\":{\"total\":12.04,\"currency\":\"USD\"},\"source\":{\"sourceType\":\"PaymentCard\",\"card\":{\"cardData\":\"[FILTERED]\",\"expirationMonth\":\"02\",\"expirationYear\":\"2035\",\"securityCode\":\"[FILTERED]\",\"securityCodeIndicator\":\"PROVIDED\"}},\"transactionInteraction\":{\"origin\":\"ECOM\",\"eciIndicator\":\"CHANNEL_ENCRYPTED\",\"posConditionCode\":\"CARD_NOT_PRESENT_ECOM\"},\"merchantDetails\":{\"terminalId\":\"10000001\",\"merchantId\":\"100008000003683\"}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Mon, 05 Dec 2022 16:48:06 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 1709\r\n" + -> "Connection: close\r\n" + -> "Expires: 0\r\n" + -> "Referrer-Policy: no-referrer\r\n" + -> "X-Frame-Options: DENY\r\n" + -> "X-Vcap-Request-Id: 30397096-5cb9-46e1-7c63-3ac2494ca38e\r\n" + -> "targetServerReceivedEndTimestamp: 1670258886388\r\n" + -> "targetServerSentStartTimestamp: 1670258885212\r\n" + -> "X-Xss-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "Cache-Control: no-store, no-cache, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "Access-Control-Max-Age: 86400\r\n" + -> "ApiTraceId: 19d178570f274a2196540af6e2e0bf55\r\n" + -> "Via: 1.1 dca1-bit16021\r\n" + -> "\r\n" + reading 1709 bytes... + -> "{\"gatewayResponse\":{\"transactionType\":\"CHARGE\",\"transactionState\":\"CAPTURED\",\"transactionOrigin\":\"ECOM\",\"transactionProcessingDetails\":{\"orderId\":\"CHG0147086beb95194e808a3bf88e052285d7\",\"transactionTimestamp\":\"2022-12-05T16:48:05.358725Z\",\"apiTraceId\":\"19d178570f274a2196540af6e2e0bf55\",\"clientRequestId\":\"3473900\",\"transactionId\":\"19d178570f274a2196540af6e2e0bf55\"}},\"source\":{\"sourceType\":\"PaymentCard\",\"card\":{\"expirationMonth\":\"02\",\"expirationYear\":\"2035\",\"securityCodeIndicator\":\"PROVIDED\",\"bin\":\"400555\",\"last4\":\"0019\",\"scheme\":\"VISA\"}},\"paymentReceipt\":{\"approvedAmount\":{\"total\":12.04,\"currency\":\"USD\"},\"processorResponseDetails\":{\"approvalStatus\":\"APPROVED\",\"approvalCode\":\"000119\",\"referenceNumber\":\"0af6e2e0bf55\",\"processor\":\"FISERV\",\"host\":\"NASHVILLE\",\"networkInternationalId\":\"0001\",\"responseCode\":\"000\",\"responseMessage\":\"Approved\",\"hostResponseCode\":\"00\",\"additionalInfo\":[{\"name\":\"HOST_RAW_PROCESSOR_RESPONSE\",\"value\":\"ARAyIAHvv70O77+9AAIAAAAAAAAAEgQSBRZIBTNCVQABWTBhZjZlMmUwYmY1NTAwMDExOTAwMDk5OTk5OTkABAACMTQ=\"}]}},\"transactionDetails\":{\"captureFlag\":true,\"transactionCaptureType\":\"hcs\",\"processingCode\":\"000000\",\"merchantInvoiceNumber\":\"995952121195\",\"createToken\":true,\"retrievalReferenceNumber\":\"0af6e2e0bf55\",\"cavvInPrimary\":false},\"transactionInteraction\":{\"posEntryMode\":\"UNSPECIFIED\",\"posConditionCode\":\"CARD_NOT_PRESENT_ECOM\",\"additionalPosInformation\":{\"dataEntrySource\":\"UNSPECIFIED\",\"posFeatures\":{\"pinAuthenticationCapability\":\"UNSPECIFIED\",\"terminalEntryCapability\":\"UNSPECIFIED\"}},\"hostPosEntryMode\":\"000\",\"hostPosConditionCode\":\"59\"},\"merchantDetails\":{\"tokenType\":\"BBY0\",\"terminalId\":\"10000001\",\"merchantId\":\"100008000003683\"},\"networkDetails\":{\"network\":{\"network\":\"Visa\"}}}" + read 1709 bytes + Conn close + POST_SCRUBBED + end +end diff --git a/test/unit/gateways/credorax_test.rb b/test/unit/gateways/credorax_test.rb index 4d01aaae1af..625953f57f0 100644 --- a/test/unit/gateways/credorax_test.rb +++ b/test/unit/gateways/credorax_test.rb @@ -42,6 +42,32 @@ def setup } } } + + @nt_credit_card = network_tokenization_credit_card( + '4176661000001015', + brand: 'visa', + eci: '07', + source: :network_token, + payment_cryptogram: 'AgAAAAAAosVKVV7FplLgQRYAAAA=' + ) + + @apple_pay_card = network_tokenization_credit_card( + '4176661000001015', + month: 10, + year: Time.new.year + 2, + first_name: 'John', + last_name: 'Smith', + verification_value: '737', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + eci: '07', + transaction_id: 'abc123', + source: :apple_pay + ) + end + + def test_supported_card_types + klass = @gateway.class + assert_equal %i[visa master maestro american_express jcb discover diners_club], klass.supported_cardtypes end def test_successful_purchase @@ -157,7 +183,7 @@ def test_successful_refund_with_recipient_fields recipient_street_address: 'street', recipient_city: 'chicago', recipient_province_code: '312', - recipient_country_code: 'USA' + recipient_country_code: 'US' } refund = stub_comms do @gateway.refund(@amount, '123', refund_options) @@ -228,6 +254,23 @@ def test_successful_credit assert response.test? end + def test_credit_sends_correct_action_code + stub_comms do + @gateway.credit(@amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_match(/O=35/, data) + end.respond_with(successful_credit_response) + end + + def test_credit_sends_customer_name + stub_comms do + @gateway.credit(@amount, @credit_card, { first_name: 'Test', last_name: 'McTest' }) + end.check_request do |_endpoint, data, _headers| + assert_match(/j5=Test/, data) + assert_match(/j13=McTest/, data) + end.respond_with(successful_credit_response) + end + def test_failed_credit response = stub_comms do @gateway.credit(@amount, @credit_card) @@ -866,7 +909,7 @@ def test_stored_credential_recurring_mit_initial end.check_request do |_endpoint, data, _headers| assert_match(/a9=1/, data) end.respond_with(successful_authorize_response) - + assert_match(/z50=abc123/, successful_authorize_response) assert_success response end @@ -964,6 +1007,7 @@ def test_stored_credential_unscheduled_mit_used @gateway.authorize(@amount, @credit_card, options) end.check_request do |_endpoint, data, _headers| assert_match(/a9=8/, data) + assert_match(/g6=abc123/, data) end.respond_with(successful_authorize_response) assert_success response @@ -975,6 +1019,7 @@ def test_purchase_with_stored_credential @gateway.purchase(@amount, @credit_card, options) end.check_request do |_endpoint, data, _headers| assert_match(/a9=2/, data) + assert_match(/g6=abc123/, data) end.respond_with(successful_authorize_response) assert_success response @@ -1026,6 +1071,26 @@ def test_3ds_2_optional_fields_does_not_empty_fields assert_equal post, {} end + def test_successful_purchase_with_network_token + response = stub_comms do + @gateway.purchase(@amount, @nt_credit_card) + end.check_request do |_endpoint, data, _headers| + assert_match(/b21=vts_mdes_token&token_eci=07&token_crypto=AgAAAAAAosVKVV7FplLgQRYAAAA%3D/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + + def test_successful_purchase_with_other_than_network_token + response = stub_comms do + @gateway.purchase(@amount, @apple_pay_card) + end.check_request do |_endpoint, data, _headers| + assert_match(/b21=applepay/, data) + assert_match(/token_eci=07/, data) + assert_not_match(/token_crypto=/, data) + end.respond_with(successful_purchase_response) + assert_success response + end + private def stored_credential_options(*args, id: nil) @@ -1047,7 +1112,7 @@ def failed_purchase_response end def successful_authorize_response - 'M=SPREE978&O=2&T=03%2F09%2F2016+03%3A08%3A58&V=413&a1=90f7449d555f7bed0a2c5d780475f0bf&a2=2&a4=100&a9=6&z1=8a829449535154bc0153595952a2517a&z13=606944188284&z14=U&z15=100&z2=0&z3=Transaction+has+been+executed+successfully.&z4=006597&z5=0&z6=00&z9=X&K=00effd2c80ab7ecd45b499c0bbea3d20' + 'M=SPREE978&O=2&T=03%2F09%2F2016+03%3A08%3A58&V=413&a1=90f7449d555f7bed0a2c5d780475f0bf&a2=2&a4=100&a9=6&z1=8a829449535154bc0153595952a2517a&z13=606944188284&z14=U&z15=100&z2=0&z3=Transaction+has+been+executed+successfully.&z4=006597&z5=0&z6=00&z9=X&K=00effd2c80ab7ecd45b499c0bbea3d20z50=abc123' end def failed_authorize_response @@ -1087,11 +1152,11 @@ def failed_referral_cft_response end def successful_credit_response - 'M=SPREE978&O=6&T=03%2F09%2F2016+03%3A16%3A35&V=413&a1=868f8b942fae639d28e27e8933d575d4&a2=2&a4=100&z1=8a82944a53515706015359604c135301&z13=606944188289&z15=100&z2=0&z3=Transaction+has+been+executed+successfully.&z5=0&z6=00&K=51ba24f6ef3aa161f86e53c34c9616ac' + 'M=SPREE978&O=35&T=03%2F09%2F2016+03%3A16%3A35&V=413&a1=868f8b942fae639d28e27e8933d575d4&a2=2&a4=100&z1=8a82944a53515706015359604c135301&z13=606944188289&z15=100&z2=0&z3=Transaction+has+been+executed+successfully.&z5=0&z6=00&K=51ba24f6ef3aa161f86e53c34c9616ac' end def failed_credit_response - 'M=SPREE978&O=6&T=03%2F09%2F2016+03%3A16%3A59&V=413&a1=ff28246cfc811b1c686a52d08d075d9c&a2=2&a4=100&z1=8a829449535154bc01535960a962524f&z13=606944188290&z15=100&z2=05&z3=Transaction+has+been+declined.&z5=0&z6=57&K=cf34816d5c25dc007ef3525505c4c610' + 'M=SPREE978&O=35&T=03%2F09%2F2016+03%3A16%3A59&V=413&a1=ff28246cfc811b1c686a52d08d075d9c&a2=2&a4=100&z1=8a829449535154bc01535960a962524f&z13=606944188290&z15=100&z2=05&z3=Transaction+has+been+declined.&z5=0&z6=57&K=cf34816d5c25dc007ef3525505c4c610' end def empty_purchase_response diff --git a/test/unit/gateways/cyber_source_rest_test.rb b/test/unit/gateways/cyber_source_rest_test.rb new file mode 100644 index 00000000000..f6ba1b40eba --- /dev/null +++ b/test/unit/gateways/cyber_source_rest_test.rb @@ -0,0 +1,585 @@ +require 'test_helper' + +class CyberSourceRestTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = CyberSourceRestGateway.new( + merchant_id: 'abc123', + public_key: 'def345', + private_key: "NYlM1sgultLjvgaraWvDCXykdz1buqOW8yXE3pMlmxQ=\n" + ) + @bank_account = check(account_number: '4100', routing_number: '121042882') + @credit_card = credit_card( + '4111111111111111', + verification_value: '987', + month: 12, + year: 2031 + ) + @apple_pay = network_tokenization_credit_card( + '4111111111111111', + payment_cryptogram: 'AceY+igABPs3jdwNaDg3MAACAAA=', + month: '11', + year: Time.now.year + 1, + source: :apple_pay, + verification_value: 569 + ) + + @google_pay_mc = network_tokenization_credit_card( + '5555555555554444', + payment_cryptogram: 'AceY+igABPs3jdwNaDg3MAACAAA=', + month: '11', + year: Time.now.year + 1, + source: :google_pay, + verification_value: 569, + brand: 'master' + ) + + @apple_pay_jcb = network_tokenization_credit_card( + '3566111111111113', + payment_cryptogram: 'AceY+igABPs3jdwNaDg3MAACAAA=', + month: '11', + year: Time.now.year + 1, + source: :apple_pay, + verification_value: 569, + brand: 'jcb' + ) + @amount = 100 + @options = { + order_id: '1', + description: 'Store Purchase', + billing_address: { + name: 'John Doe', + address1: '1 Market St', + city: 'san francisco', + state: 'CA', + zip: '94105', + country: 'US', + phone: '4158880000' + }, + email: 'test@cybs.com' + } + @gmt_time = Time.now.httpdate + @digest = 'SHA-256=gXWufV4Zc7VkN9Wkv9jh/JuAVclqDusx3vkyo3uJFWU=' + @resource = '/pts/v2/payments/' + end + + def test_required_merchant_id_and_secret + error = assert_raises(ArgumentError) { CyberSourceRestGateway.new } + assert_equal 'Missing required parameter: merchant_id', error.message + end + + def test_supported_card_types + assert_equal CyberSourceRestGateway.supported_cardtypes, %i[visa master american_express discover diners_club jcb maestro elo union_pay cartes_bancaires mada] + end + + def test_properly_format_on_zero_decilmal + stub_comms do + @gateway.authorize(1000, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + card = request['paymentInformation']['card'] + amount_details = request['orderInformation']['amountDetails'] + + assert_equal '1', request['clientReferenceInformation']['code'] + assert_equal '2031', card['expirationYear'] + assert_equal '12', card['expirationMonth'] + assert_equal '987', card['securityCode'] + assert_equal '001', card['type'] + assert_equal 'USD', amount_details['currency'] + assert_equal '10.00', amount_details['totalAmount'] + end.respond_with(successful_purchase_response) + end + + def test_should_create_an_http_signature_for_a_post + signature = @gateway.send :get_http_signature, @resource, @digest, 'post', @gmt_time + + parsed = parse_signature(signature) + + assert_equal 'def345', parsed['keyid'] + assert_equal 'HmacSHA256', parsed['algorithm'] + assert_equal 'host date (request-target) digest v-c-merchant-id', parsed['headers'] + assert_equal %w[algorithm headers keyid signature], signature.split(', ').map { |v| v.split('=').first }.sort + end + + def test_should_create_an_http_signature_for_a_get + signature = @gateway.send :get_http_signature, @resource, nil, 'get', @gmt_time + + parsed = parse_signature(signature) + assert_equal 'host date (request-target) v-c-merchant-id', parsed['headers'] + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_including_customer_if_customer_id_present + post = { paymentInformation: {} } + + @gateway.send :add_customer_id, post, {} + assert_nil post[:paymentInformation][:customer] + + @gateway.send :add_customer_id, post, { customer_id: 10 } + assert_equal 10, post[:paymentInformation][:customer][:customerId] + end + + def test_add_ammount_and_currency + post = { orderInformation: {} } + + @gateway.send :add_amount, post, 10221, {} + + assert_equal '102.21', post.dig(:orderInformation, :amountDetails, :totalAmount) + assert_equal 'USD', post.dig(:orderInformation, :amountDetails, :currency) + end + + def test_add_credit_card_data + post = { paymentInformation: {} } + @gateway.send :add_credit_card, post, @credit_card + + card = post[:paymentInformation][:card] + assert_equal @credit_card.number, card[:number] + assert_equal '2031', card[:expirationYear] + assert_equal '12', card[:expirationMonth] + assert_equal '987', card[:securityCode] + assert_equal '001', card[:type] + end + + def test_add_ach + post = { paymentInformation: {} } + @gateway.send :add_ach, post, @bank_account + + bank = post[:paymentInformation][:bank] + assert_equal @bank_account.account_number, bank[:account][:number] + assert_equal @bank_account.routing_number, bank[:routingNumber] + end + + def test_add_billing_address + post = { orderInformation: {} } + + @gateway.send :add_address, post, @credit_card, @options[:billing_address], @options, :billTo + + address = post[:orderInformation][:billTo] + + assert_equal 'John', address[:firstName] + assert_equal 'Doe', address[:lastName] + assert_equal '1 Market St', address[:address1] + assert_equal 'san francisco', address[:locality] + assert_equal 'US', address[:country] + assert_equal 'test@cybs.com', address[:email] + assert_equal '4158880000', address[:phoneNumber] + end + + def test_add_shipping_address + post = { orderInformation: {} } + @options[:shipping_address] = @options.delete(:billing_address) + + @gateway.send :add_address, post, @credit_card, @options[:shipping_address], @options, :shipTo + + address = post[:orderInformation][:shipTo] + + assert_equal 'John', address[:firstName] + assert_equal 'Doe', address[:lastName] + assert_equal '1 Market St', address[:address1] + assert_equal 'san francisco', address[:locality] + assert_equal 'US', address[:country] + assert_equal 'test@cybs.com', address[:email] + assert_equal '4158880000', address[:phoneNumber] + end + + def test_authorize_apple_pay_visa + stub_comms do + @gateway.authorize(100, @apple_pay, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '001', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '1', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_equal 'AceY+igABPs3jdwNaDg3MAACAAA=', request['paymentInformation']['tokenizedCard']['cryptogram'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '001', request['processingInformation']['paymentSolution'] + assert_equal 'internet', request['processingInformation']['commerceIndicator'] + assert_include request['consumerAuthenticationInformation'], 'cavv' + end.respond_with(successful_purchase_response) + end + + def test_authorize_google_pay_master_card + stub_comms do + @gateway.authorize(100, @google_pay_mc, @options.merge(merchant_id: 'MerchantId')) + end.check_request do |_endpoint, data, headers| + request = JSON.parse(data) + assert_equal 'MerchantId', headers['V-C-Merchant-Id'] + assert_equal '002', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '1', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '012', request['processingInformation']['paymentSolution'] + assert_equal 'internet', request['processingInformation']['commerceIndicator'] + assert_equal request['consumerAuthenticationInformation']['ucafCollectionIndicator'], '2' + assert_include request['consumerAuthenticationInformation'], 'ucafAuthenticationData' + end.respond_with(successful_purchase_response) + end + + def test_authorize_apple_pay_jcb + stub_comms do + @gateway.authorize(100, @apple_pay_jcb, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '007', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '1', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '001', request['processingInformation']['paymentSolution'] + assert_nil request['processingInformation']['commerceIndicator'] + assert_include request['consumerAuthenticationInformation'], 'cavv' + end.respond_with(successful_purchase_response) + end + + def test_url_building + assert_equal "#{@gateway.class.test_url}/pts/v2/action", @gateway.send(:url, 'action') + end + + def test_stored_credential_cit_initial + @options[:stored_credential] = stored_credential(:cardholder, :internet, :initial) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal 'internet', request['processingInformation']['commerceIndicator'] + assert_equal 'customer', request.dig('processingInformation', 'authorizationOptions', 'initiator', 'type') + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_recurring_cit + @options[:stored_credential] = stored_credential(:cardholder, :recurring) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal 'recurring', request['processingInformation']['commerceIndicator'] + assert_equal 'customer', request.dig('processingInformation', 'authorizationOptions', 'initiator', 'type') + assert_equal true, request.dig('processingInformation', 'authorizationOptions', 'initiator', 'merchantInitiatedTransaction', 'storedCredentialUsed') + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_stored_credential_recurring_mit_ntid + @options[:stored_credential] = stored_credential(:merchant, :recurring, ntid: '123456789619999') + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal 'recurring', request['processingInformation']['commerceIndicator'] + assert_equal 'merchant', request.dig('processingInformation', 'authorizationOptions', 'initiator', 'type') + assert_equal true, request.dig('processingInformation', 'authorizationOptions', 'initiator', 'merchantInitiatedTransaction', 'storedCredentialUsed') + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_credit_card_purchase_single_request_ignore_avs + stub_comms do + options = @options.merge(ignore_avs: true) + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, request_body, _headers| + json_body = JSON.parse(request_body) + assert_equal json_body['processingInformation']['authorizationOptions']['ignoreAvsResult'], 'true' + assert_nil json_body['processingInformation']['authorizationOptions']['ignoreCvResult'] + end.respond_with(successful_purchase_response) + end + + def test_successful_credit_card_purchase_single_request_without_ignore_avs + stub_comms do + # globally ignored AVS for gateway instance: + options = @options.merge(ignore_avs: false) + @gateway.options[:ignore_avs] = true + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, request_body, _headers| + json_body = JSON.parse(request_body) + assert_nil json_body['processingInformation']['authorizationOptions']['ignoreAvsResult'] + assert_nil json_body['processingInformation']['authorizationOptions']['ignoreCvResult'] + end.respond_with(successful_purchase_response) + end + + def test_successful_credit_card_purchase_single_request_ignore_ccv + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(ignore_cvv: true)) + end.check_request do |_endpoint, request_body, _headers| + json_body = JSON.parse(request_body) + assert_nil json_body['processingInformation']['authorizationOptions']['ignoreAvsResult'] + assert_equal json_body['processingInformation']['authorizationOptions']['ignoreCvResult'], 'true' + end.respond_with(successful_purchase_response) + end + + def test_successful_credit_card_purchase_single_request_without_ignore_ccv + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(ignore_cvv: false)) + end.check_request do |_endpoint, request_body, _headers| + json_body = JSON.parse(request_body) + assert_nil json_body['processingInformation']['authorizationOptions']['ignoreAvsResult'] + assert_nil json_body['processingInformation']['authorizationOptions']['ignoreCvResult'] + end.respond_with(successful_purchase_response) + end + + def test_authorize_includes_mdd_fields + stub_comms do + @gateway.authorize(100, @credit_card, order_id: '1', mdd_field_2: 'CustomValue2', mdd_field_3: 'CustomValue3') + end.check_request do |_endpoint, data, _headers| + json_data = JSON.parse(data) + assert_equal json_data['merchantDefinedInformation'][0]['key'], 'mdd_field_2' + assert_equal json_data['merchantDefinedInformation'][0]['value'], 'CustomValue2' + assert_equal json_data['merchantDefinedInformation'].count, 2 + end.respond_with(successful_purchase_response) + end + + def test_capture_includes_mdd_fields + stub_comms do + @gateway.capture(100, '1846925324700976124593', order_id: '1', mdd_field_2: 'CustomValue2', mdd_field_3: 'CustomValue3') + end.check_request do |_endpoint, data, _headers| + json_data = JSON.parse(data) + assert_equal json_data['merchantDefinedInformation'][0]['key'], 'mdd_field_2' + assert_equal json_data['merchantDefinedInformation'][0]['value'], 'CustomValue2' + assert_equal json_data['merchantDefinedInformation'].count, 2 + end.respond_with(successful_capture_response) + end + + def test_credit_includes_mdd_fields + stub_comms do + @gateway.credit(@amount, @credit_card, mdd_field_2: 'CustomValue2', mdd_field_3: 'CustomValue3') + end.check_request do |_endpoint, data, _headers| + json_data = JSON.parse(data) + assert_equal json_data['merchantDefinedInformation'][0]['key'], 'mdd_field_2' + assert_equal json_data['merchantDefinedInformation'][0]['value'], 'CustomValue2' + assert_equal json_data['merchantDefinedInformation'].count, 2 + end.respond_with(successful_credit_response) + end + + def test_authorize_includes_reconciliation_id + stub_comms do + @gateway.authorize(100, @credit_card, order_id: '1', reconciliation_id: '181537') + end.check_request do |_endpoint, data, _headers| + json_data = JSON.parse(data) + assert_equal json_data['clientReferenceInformation']['reconciliationId'], '181537' + end.respond_with(successful_purchase_response) + end + + def test_bank_account_purchase_includes_sec_code + stub_comms do + @gateway.purchase(@amount, @bank_account, order_id: '1', sec_code: 'WEB') + end.check_request do |_endpoint, data, _headers| + json_data = JSON.parse(data) + assert_equal json_data['processingInformation']['bankTransferOptions']['secCode'], 'WEB' + end.respond_with(successful_purchase_response) + end + + def test_purchase_includes_invoice_number + stub_comms do + @gateway.purchase(100, @credit_card, invoice_number: '1234567') + end.check_request do |_endpoint, data, _headers| + json_data = JSON.parse(data) + assert_equal json_data['orderInformation']['invoiceDetails']['invoiceNumber'], '1234567' + end.respond_with(successful_purchase_response) + end + + def test_adds_application_id_as_partner_solution_id + partner_id = 'partner_id' + CyberSourceRestGateway.application_id = partner_id + + stub_comms do + @gateway.authorize(100, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + json_data = JSON.parse(data) + assert_equal json_data['clientReferenceInformation']['partner']['solutionId'], partner_id + end.respond_with(successful_purchase_response) + ensure + CyberSourceRestGateway.application_id = nil + end + + private + + def parse_signature(signature) + signature.gsub(/=\"$/, '').delete('"').split(', ').map { |x| x.split('=') }.to_h + end + + def pre_scrubbed + <<-PRE + <- "POST /pts/v2/payments/ HTTP/1.1\r\nContent-Type: application/json;charset=utf-8\r\nAccept: application/hal+json;charset=utf-8\r\nV-C-Merchant-Id: testrest\r\nDate: Sun, 29 Jan 2023 17:13:30 GMT\r\nHost: apitest.cybersource.com\r\nSignature: keyid=\"08c94330-f618-42a3-b09d-e1e43be5efda\", algorithm=\"HmacSHA256\", headers=\"host date (request-target) digest v-c-merchant-id\", signature=\"DJHeHWceVrsJydd8BCbGowr9dzQ/ry5cGN1FocLakEw=\"\r\nDigest: SHA-256=wuV1cxGzs6KpuUKJmlD7pKV6MZ/5G1wQVoYbf8cRChM=\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nContent-Length: 584\r\n\r\n" + <- "{\"clientReferenceInformation\":{\"code\":\"b8779865d140125036016a0f85db907f\"},\"paymentInformation\":{\"card\":{\"number\":\"4111111111111111\",\"expirationMonth\":\"12\",\"expirationYear\":\"2031\",\"securityCode\":\"987\",\"type\":\"001\"}},\"orderInformation\":{\"amountDetails\":{\"totalAmount\":\"102.21\",\"currency\":\"USD\"},\"billTo\":{\"firstName\":\"John\",\"lastName\":\"Doe\",\"address1\":\"1 Market St\",\"locality\":\"san francisco\",\"administrativeArea\":\"CA\",\"postalCode\":\"94105\",\"country\":\"US\",\"email\":\"test@cybs.com\",\"phoneNumber\":\"4158880000\"},\"shipTo\":{\"firstName\":\"Longbob\",\"lastName\":\"Longsen\",\"email\":\"test@cybs.com\"}}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Cache-Control: no-cache, no-store, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "Expires: -1\r\n" + -> "Strict-Transport-Security: max-age=31536000\r\n" + -> "Content-Type: application/hal+json\r\n" + -> "Content-Length: 905\r\n" + -> "x-response-time: 291ms\r\n" + -> "X-OPNET-Transaction-Trace: 0b1f2bd7-9545-4939-9478-4b76cf7199b6\r\n" + -> "Connection: close\r\n" + -> "v-c-correlation-id: 42969bf5-a77d-4035-9d09-58d4ca070e8c\r\n" + -> "\r\n" + reading 905 bytes... + -> "{\"_links\":{\"authReversal\":{\"method\":\"POST\",\"href\":\"/pts/v2/payments/6750124114786780104953/reversals\"},\"self\":{\"method\":\"GET\",\"href\":\"/pts/v2/payments/6750124114786780104953\"},\"capture\":{\"method\":\"POST\",\"href\":\"/pts/v2/payments/6750124114786780104953/captures\"}},\"clientReferenceInformation\":{\"code\":\"b8779865d140125036016a0f85db907f\"},\"id\":\"6750124114786780104953\",\"orderInformation\":{\"amountDetails\":{\"authorizedAmount\":\"102.21\",\"currency\":\"USD\"}},\"paymentAccountInformation\":{\"card\":{\"type\":\"001\"}},\"paymentInformation\":{\"tokenizedCard\":{\"type\":\"001\"},\"card\":{\"type\":\"001\"}},\"pointOfSaleInformation\":{\"terminalId\":\"111111\"},\"processorInformation\":{\"approvalCode\":\"888888\",\"networkTransactionId\":\"123456789619999\",\"transactionId\":\"123456789619999\",\"responseCode\":\"100\",\"avs\":{\"code\":\"X\",\"codeRaw\":\"I1\"}},\"reconciliationId\":\"78243988SD9YL291\",\"status\":\"AUTHORIZED\",\"submitTimeUtc\":\"2023-01-29T17:13:31Z\"}" + PRE + end + + def post_scrubbed + <<-POST + <- "POST /pts/v2/payments/ HTTP/1.1\r\nContent-Type: application/json;charset=utf-8\r\nAccept: application/hal+json;charset=utf-8\r\nV-C-Merchant-Id: testrest\r\nDate: Sun, 29 Jan 2023 17:13:30 GMT\r\nHost: apitest.cybersource.com\r\nSignature: keyid=\"[FILTERED]\", algorithm=\"HmacSHA256\", headers=\"host date (request-target) digest v-c-merchant-id\", signature=\"[FILTERED]\"\r\nDigest: SHA-256=[FILTERED]\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nContent-Length: 584\r\n\r\n" + <- "{\"clientReferenceInformation\":{\"code\":\"b8779865d140125036016a0f85db907f\"},\"paymentInformation\":{\"card\":{\"number\":\"[FILTERED]\",\"expirationMonth\":\"12\",\"expirationYear\":\"2031\",\"securityCode\":\"[FILTERED]\",\"type\":\"001\"}},\"orderInformation\":{\"amountDetails\":{\"totalAmount\":\"102.21\",\"currency\":\"USD\"},\"billTo\":{\"firstName\":\"John\",\"lastName\":\"Doe\",\"address1\":\"1 Market St\",\"locality\":\"san francisco\",\"administrativeArea\":\"CA\",\"postalCode\":\"94105\",\"country\":\"US\",\"email\":\"test@cybs.com\",\"phoneNumber\":\"4158880000\"},\"shipTo\":{\"firstName\":\"Longbob\",\"lastName\":\"Longsen\",\"email\":\"test@cybs.com\"}}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Cache-Control: no-cache, no-store, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "Expires: -1\r\n" + -> "Strict-Transport-Security: max-age=31536000\r\n" + -> "Content-Type: application/hal+json\r\n" + -> "Content-Length: 905\r\n" + -> "x-response-time: 291ms\r\n" + -> "X-OPNET-Transaction-Trace: 0b1f2bd7-9545-4939-9478-4b76cf7199b6\r\n" + -> "Connection: close\r\n" + -> "v-c-correlation-id: 42969bf5-a77d-4035-9d09-58d4ca070e8c\r\n" + -> "\r\n" + reading 905 bytes... + -> "{\"_links\":{\"authReversal\":{\"method\":\"POST\",\"href\":\"/pts/v2/payments/6750124114786780104953/reversals\"},\"self\":{\"method\":\"GET\",\"href\":\"/pts/v2/payments/6750124114786780104953\"},\"capture\":{\"method\":\"POST\",\"href\":\"/pts/v2/payments/6750124114786780104953/captures\"}},\"clientReferenceInformation\":{\"code\":\"b8779865d140125036016a0f85db907f\"},\"id\":\"6750124114786780104953\",\"orderInformation\":{\"amountDetails\":{\"authorizedAmount\":\"102.21\",\"currency\":\"USD\"}},\"paymentAccountInformation\":{\"card\":{\"type\":\"001\"}},\"paymentInformation\":{\"tokenizedCard\":{\"type\":\"001\"},\"card\":{\"type\":\"001\"}},\"pointOfSaleInformation\":{\"terminalId\":\"111111\"},\"processorInformation\":{\"approvalCode\":\"888888\",\"networkTransactionId\":\"123456789619999\",\"transactionId\":\"123456789619999\",\"responseCode\":\"100\",\"avs\":{\"code\":\"X\",\"codeRaw\":\"I1\"}},\"reconciliationId\":\"78243988SD9YL291\",\"status\":\"AUTHORIZED\",\"submitTimeUtc\":\"2023-01-29T17:13:31Z\"}" + POST + end + + def successful_purchase_response + <<-RESPONSE + { + "_links": { + "authReversal": { + "method": "POST", + "href": "/pts/v2/payments/6750124114786780104953/reversals" + }, + "self": { + "method": "GET", + "href": "/pts/v2/payments/6750124114786780104953" + }, + "capture": { + "method": "POST", + "href": "/pts/v2/payments/6750124114786780104953/captures" + } + }, + "clientReferenceInformation": { + "code": "b8779865d140125036016a0f85db907f" + }, + "id": "6750124114786780104953", + "orderInformation": { + "amountDetails": { + "authorizedAmount": "102.21", + "currency": "USD" + } + }, + "paymentAccountInformation": { + "card": { + "type": "001" + } + }, + "paymentInformation": { + "tokenizedCard": { + "type": "001" + }, + "card": { + "type": "001" + } + }, + "pointOfSaleInformation": { + "terminalId": "111111" + }, + "processorInformation": { + "approvalCode": "888888", + "networkTransactiDDDonId": "123456789619999", + "transactionId": "123456789619999", + "responseCode": "100", + "avs": { + "code": "X", + "codeRaw": "I1" + } + }, + "reconciliationId": "78243988SD9YL291", + "status": "AUTHORIZED", + "submitTimeUtc": "2023-01-29T17:13:31Z" + } + RESPONSE + end + + def successful_capture_response + <<-RESPONSE + { + "_links": { + "void": { + "method": "POST", + "href": "/pts/v2/captures/6799471903876585704951/voids" + }, + "self": { + "method": "GET", + "href": "/pts/v2/captures/6799471903876585704951" + } + }, + "clientReferenceInformation": { + "code": "TC50171_3" + }, + "id": "6799471903876585704951", + "orderInformation": { + "amountDetails": { + "totalAmount": "102.21", + "currency": "USD" + } + }, + "reconciliationId": "78243988SD9YL291", + "status": "PENDING", + "submitTimeUtc": "2023-03-27T19:59:50Z" + } + RESPONSE + end + + def successful_credit_response + <<-RESPONSE + { + "_links": { + "void": { + "method": "POST", + "href": "/pts/v2/credits/6799499091686234304951/voids" + }, + "self": { + "method": "GET", + "href": "/pts/v2/credits/6799499091686234304951" + } + }, + "clientReferenceInformation": { + "code": "12345678" + }, + "creditAmountDetails": { + "currency": "usd", + "creditAmount": "200.00" + }, + "id": "6799499091686234304951", + "orderInformation": { + "amountDetails": { + "currency": "usd" + } + }, + "paymentAccountInformation": { + "card": { + "type": "001" + } + }, + "paymentInformation": { + "tokenizedCard": { + "type": "001" + }, + "card": { + "type": "001" + } + }, + "processorInformation": { + "approvalCode": "888888", + "responseCode": "100" + }, + "reconciliationId": "70391830ZFKZI570", + "status": "PENDING", + "submitTimeUtc": "2023-03-27T20:45:09Z" + } + RESPONSE + end +end diff --git a/test/unit/gateways/cyber_source_test.rb b/test/unit/gateways/cyber_source_test.rb index c22ee9b5e28..0e67c44700c 100644 --- a/test/unit/gateways/cyber_source_test.rb +++ b/test/unit/gateways/cyber_source_test.rb @@ -18,6 +18,19 @@ def setup @master_credit_card = credit_card('4111111111111111', brand: 'master') @elo_credit_card = credit_card('5067310000000010', brand: 'elo') @declined_card = credit_card('801111111111111', brand: 'visa') + @network_token = network_tokenization_credit_card('4111111111111111', + brand: 'visa', + transaction_id: '123', + eci: '05', + payment_cryptogram: '111111111100cryptogram', + source: :network_token) + @apple_pay = network_tokenization_credit_card('4111111111111111', + brand: 'visa', + transaction_id: '123', + eci: '05', + payment_cryptogram: '111111111100cryptogram', + source: :apple_pay) + @google_pay = network_tokenization_credit_card('4242424242424242', source: :google_pay) @check = check() @options = { @@ -60,16 +73,24 @@ def test_successful_credit_card_purchase assert response = @gateway.purchase(@amount, @credit_card, @options) assert_equal 'Successful transaction', response.message assert_success response - assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;", response.authorization + assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;;credit_card", response.authorization assert response.test? end - def test_successful_purchase_with_national_tax_indicator - national_tax_indicator = 1 + def test_successful_purchase_with_other_tax_fields stub_comms do - @gateway.purchase(100, @credit_card, @options.merge(national_tax_indicator: national_tax_indicator)) + @gateway.purchase(100, @credit_card, @options.merge!(national_tax_indicator: 1, vat_tax_rate: 1.01, merchant_id: 'MerchantId')) end.check_request do |_endpoint, data, _headers| - assert_match(/\s+#{national_tax_indicator}<\/nationalTaxIndicator>\s+<\/otherTax>/m, data) + assert_match(/MerchantId<\/merchantID>/, data) + assert_match(/\s+1.01<\/vatTaxRate>\s+1<\/nationalTaxIndicator>\s+<\/otherTax>/m, data) + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_purchase_totals_data + stub_comms do + @gateway.purchase(100, @credit_card, @options.merge(discount_management_indicator: 'T', purchase_tax_amount: 7.89, original_amount: 1.23, invoice_amount: 1.23)) + end.check_request do |_endpoint, data, _headers| + assert_match(/\s+USD<\/currency>\s+T<\/discountManagementIndicator>\s+7.89<\/taxAmount>\s+1.00<\/grandTotalAmount>\s+1.23<\/originalAmount>\s+1.23<\/invoiceAmount>\s+<\/purchaseTotals>/m, data) end.respond_with(successful_purchase_response) end @@ -82,13 +103,21 @@ def test_successful_authorize_with_national_tax_indicator end.respond_with(successful_authorization_response) end + def test_successful_authorize_with_cc_auth_service_fields + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(mobile_remote_payment_type: 'T')) + end.check_request do |_endpoint, data, _headers| + assert_match(/T<\/mobileRemotePaymentType>/, data) + end.respond_with(successful_authorization_response) + end + def test_successful_credit_card_purchase_with_elo @gateway.expects(:ssl_post).returns(successful_purchase_response) assert response = @gateway.purchase(@amount, @elo_credit_card, @options) assert_equal 'Successful transaction', response.message assert_success response - assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;", response.authorization + assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;;credit_card", response.authorization assert response.test? end @@ -189,11 +218,37 @@ def test_uses_names_from_the_payment_method end.respond_with(successful_purchase_response) end - def test_purchase_includes_merchant_descriptor + def test_purchase_includes_invoice_header stub_comms do - @gateway.purchase(100, @credit_card, merchant_descriptor: 'Spreedly') + @gateway.purchase(100, @credit_card, merchant_descriptor: 'Spreedly', reference_data_code: '3A', invoice_number: '1234567') end.check_request do |_endpoint, data, _headers| assert_match(/Spreedly<\/merchantDescriptor>/, data) + assert_match(/3A<\/referenceDataCode>/, data) + assert_match(/1234567<\/invoiceNumber>/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_apple_pay_includes_payment_solution_001 + stub_comms do + @gateway.purchase(100, @apple_pay) + end.check_request do |_endpoint, data, _headers| + assert_match(/001<\/paymentSolution>/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_google_pay_includes_payment_solution_012 + stub_comms do + @gateway.purchase(100, @google_pay) + end.check_request do |_endpoint, data, _headers| + assert_match(/012<\/paymentSolution>/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_includes_tax_management_indicator + stub_comms do + @gateway.purchase(100, @credit_card, tax_management_indicator: 3) + end.check_request do |_endpoint, data, _headers| + assert_match(/3<\/taxManagementIndicator>/, data) end.respond_with(successful_purchase_response) end @@ -231,9 +286,19 @@ def test_authorize_includes_commerce_indicator def test_authorize_includes_installment_data stub_comms do - @gateway.authorize(100, @credit_card, order_id: '1', installment_total_count: 5, installment_plan_type: 1, first_installment_date: '300101') + @gateway.authorize(100, @credit_card, order_id: '1', installment_total_count: 5, installment_plan_type: 1, first_installment_date: '300101', installment_total_amount: 5.05, installment_annual_interest_rate: 1.09, installment_grace_period_duration: 3) end.check_request do |_endpoint, data, _headers| - assert_match(/\s+5<\/totalCount>\s+1<\/planType>\s+300101<\/firstInstallmentDate>\s+<\/installment>/, data) + assert_xml_valid_to_xsd(data) + assert_match(/\s+5<\/totalCount>\s+5.05<\/totalAmount>\s+1<\/planType>\s+300101<\/firstInstallmentDate>\s+1.09<\/annualInterestRate>\s+3<\/gracePeriodDuration>\s+<\/installment>/, data) + end.respond_with(successful_authorization_response) + end + + def test_authorize_includes_less_installment_data + stub_comms do + @gateway.authorize(100, @credit_card, order_id: '1', installment_grace_period_duration: 3) + end.check_request do |_endpoint, data, _headers| + assert_xml_valid_to_xsd(data) + assert_match(/\s+3<\/gracePeriodDuration>\s+<\/installment>/, data) end.respond_with(successful_authorization_response) end @@ -245,6 +310,22 @@ def test_authorize_includes_customer_id end.respond_with(successful_authorization_response) end + def test_authorize_with_apple_pay_includes_payment_solution_001 + stub_comms do + @gateway.authorize(100, @apple_pay) + end.check_request do |_endpoint, data, _headers| + assert_match(/001<\/paymentSolution>/, data) + end.respond_with(successful_authorization_response) + end + + def test_authorize_with_google_pay_includes_payment_solution_012 + stub_comms do + @gateway.authorize(100, @google_pay) + end.check_request do |_endpoint, data, _headers| + assert_match(/012<\/paymentSolution>/, data) + end.respond_with(successful_authorization_response) + end + def test_authorize_includes_merchant_tax_id_in_billing_address_but_not_shipping_address stub_comms do @gateway.authorize(100, @credit_card, order_id: '1', merchant_tax_id: '123') @@ -270,13 +351,21 @@ def test_authorize_includes_airline_agent_code end.respond_with(successful_authorization_response) end + def test_bank_account_purchase_includes_sec_code + stub_comms do + @gateway.purchase(@amount, @check, order_id: '1', sec_code: 'WEB') + end.check_request do |_endpoint, data, _headers| + assert_match(%r(.*WEB.*)m, data) + end.respond_with(successful_authorization_response) + end + def test_successful_check_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) assert response = @gateway.purchase(@amount, @check, @options) assert_equal 'Successful transaction', response.message assert_success response - assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;", response.authorization + assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;;check", response.authorization assert response.test? end @@ -286,7 +375,7 @@ def test_successful_pinless_debit_card_purchase assert response = @gateway.purchase(@amount, @credit_card, @options.merge(pinless_debit_card: true)) assert_equal 'Successful transaction', response.message assert_success response - assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;", response.authorization + assert_equal "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};purchase;100;USD;;credit_card", response.authorization assert response.test? end @@ -302,6 +391,18 @@ def test_successful_credit_cart_purchase_single_request_ignore_avs assert_success response end + def test_successful_network_token_purchase_single_request_ignore_avs + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_match %r'true', request_body + assert_not_match %r'', request_body + true + end.returns(successful_purchase_response) + + options = @options.merge(ignore_avs: true) + assert response = @gateway.purchase(@amount, @network_token, options) + assert_success response + end + def test_successful_credit_cart_purchase_single_request_without_ignore_avs @gateway.expects(:ssl_post).with do |_host, request_body| assert_not_match %r'', request_body @@ -334,9 +435,19 @@ def test_successful_credit_cart_purchase_single_request_ignore_ccv true end.returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options.merge( - ignore_cvv: true - )) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(ignore_cvv: true)) + assert_success response + end + + def test_successful_network_token_purchase_single_request_ignore_cvv + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_not_match %r'', request_body + assert_match %r'true', request_body + true + end.returns(successful_purchase_response) + + options = @options.merge(ignore_cvv: true) + assert response = @gateway.purchase(@amount, @network_token, options) assert_success response end @@ -347,9 +458,7 @@ def test_successful_credit_cart_purchase_single_request_without_ignore_ccv true end.returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options.merge( - ignore_cvv: false - )) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(ignore_cvv: false)) assert_success response @gateway.expects(:ssl_post).with do |_host, request_body| @@ -358,9 +467,69 @@ def test_successful_credit_cart_purchase_single_request_without_ignore_ccv true end.returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options.merge( - ignore_cvv: 'false' - )) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(ignore_cvv: 'false')) + assert_success response + end + + def test_successful_apple_pay_purchase_subsequent_auth_visa + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_not_match %r'', request_body + assert_not_match %r'', request_body + assert_match %r'internet', request_body + true + end.returns(successful_purchase_response) + + options = @options.merge({ + stored_credential: { + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '016150703802094' + } + }) + assert response = @gateway.purchase(@amount, @apple_pay, options) + assert_success response + end + + def test_successful_apple_pay_purchase_subsequent_auth_mastercard + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_not_match %r'', request_body + assert_match %r'internet', request_body + true + end.returns(successful_purchase_response) + + credit_card = network_tokenization_credit_card('5555555555554444', + brand: 'master', + transaction_id: '123', + eci: '05', + payment_cryptogram: '111111111100cryptogram', + source: :apple_pay) + options = @options.merge({ + stored_credential: { + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '016150703802094' + } + }) + assert response = @gateway.purchase(@amount, credit_card, options) + assert_success response + end + + def test_successful_network_token_purchase_subsequent_auth_visa + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_match %r'111111111100cryptogram', request_body + assert_match %r'vbv', request_body + assert_not_match %r'internet', request_body + true + end.returns(successful_purchase_response) + + options = @options.merge({ + stored_credential: { + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '016150703802094' + } + }) + assert response = @gateway.purchase(@amount, @network_token, options) assert_success response end @@ -477,6 +646,14 @@ def test_capture_with_additional_tax_fields end.respond_with(successful_capture_response) end + def test_capture_includes_gratuity_amount + stub_comms do + @gateway.capture(100, '1842651133440156177166', gratuity_amount: '3.05') + end.check_request do |_endpoint, data, _headers| + assert_match(/3.05<\/gratuityAmount>/, data) + end.respond_with(successful_capture_response) + end + def test_successful_credit_card_capture_with_elo_request @gateway.stubs(:ssl_post).returns(successful_authorization_response, successful_capture_response) assert response = @gateway.authorize(@amount, @elo_credit_card, @options) @@ -620,7 +797,7 @@ def test_authorization_under_review_request assert_failure(response = @gateway.authorize(@amount, @credit_card, @options)) assert response.fraud_review? - assert_equal(response.authorization, "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};authorize;100;USD;") + assert_equal(response.authorization, "#{@options[:order_id]};#{response.params['requestID']};#{response.params['requestToken']};authorize;100;USD;;") end def test_successful_credit_to_subscription_request @@ -763,31 +940,19 @@ def test_unsuccessful_verify end def test_successful_auth_with_network_tokenization_for_visa - credit_card = network_tokenization_credit_card('4111111111111111', - brand: 'visa', - transaction_id: '123', - eci: '05', - payment_cryptogram: '111111111100cryptogram') - response = stub_comms do - @gateway.authorize(@amount, credit_card, @options) + @gateway.authorize(@amount, @network_token, @options) end.check_request do |_endpoint, body, _headers| assert_xml_valid_to_xsd(body) - assert_match %r'\n 111111111100cryptogram\n vbv\n 111111111100cryptogram\n\n\n 1\n', body + assert_match %r'\n 111111111100cryptogram\n vbv\n 111111111100cryptogram\n\n\n\n\n 1\n', body end.respond_with(successful_purchase_response) assert_success response end def test_successful_purchase_with_network_tokenization_for_visa - credit_card = network_tokenization_credit_card('4111111111111111', - brand: 'visa', - transaction_id: '123', - eci: '05', - payment_cryptogram: '111111111100cryptogram') - response = stub_comms do - @gateway.purchase(@amount, credit_card, @options) + @gateway.purchase(@amount, @network_token, @options) end.check_request do |_endpoint, body, _headers| assert_xml_valid_to_xsd(body) assert_match %r'.+?'m, body @@ -799,15 +964,17 @@ def test_successful_purchase_with_network_tokenization_for_visa def test_successful_auth_with_network_tokenization_for_mastercard @gateway.expects(:ssl_post).with do |_host, request_body| assert_xml_valid_to_xsd(request_body) - assert_match %r'\n 111111111100cryptogram\n 2\n\n\n spa\n\n\n 1\n', request_body + assert_match %r'\n 111111111100cryptogram\n 2\n\n\n spa\n\n\n\n\n 1\n', request_body true end.returns(successful_purchase_response) - credit_card = network_tokenization_credit_card('5555555555554444', + credit_card = network_tokenization_credit_card( + '5555555555554444', brand: 'master', transaction_id: '123', eci: '05', - payment_cryptogram: '111111111100cryptogram') + payment_cryptogram: '111111111100cryptogram' + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response @@ -816,15 +983,17 @@ def test_successful_auth_with_network_tokenization_for_mastercard def test_successful_auth_with_network_tokenization_for_amex @gateway.expects(:ssl_post).with do |_host, request_body| assert_xml_valid_to_xsd(request_body) - assert_match %r'\n MTExMTExMTExMTAwY3J5cHRvZ3I=\n\n aesk\n YW0=\n\n\n\n 1\n', request_body + assert_match %r'\n MTExMTExMTExMTAwY3J5cHRvZ3I=\n\n aesk\n YW0=\n\n\n\n\n\n 1\n', request_body true end.returns(successful_purchase_response) - credit_card = network_tokenization_credit_card('378282246310005', + credit_card = network_tokenization_credit_card( + '378282246310005', brand: 'american_express', transaction_id: '123', eci: '05', - payment_cryptogram: Base64.encode64('111111111100cryptogram')) + payment_cryptogram: Base64.encode64('111111111100cryptogram') + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response @@ -1251,6 +1420,82 @@ def test_does_not_add_cavv_as_xid_if_xid_is_present end.respond_with(successful_purchase_response) end + def test_add_3ds_exemption_fields_except_stored_credential + CyberSourceGateway::THREEDS_EXEMPTIONS.keys.reject { |k| k == :stored_credential }.each do |exemption| + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(options_with_normalized_3ds, three_ds_exemption_type: exemption.to_s, merchant_id: 'test', billing_address: { + 'address1' => '221B Baker Street', + 'city' => 'London', + 'zip' => 'NW16XE', + 'country' => 'GB' + })) + end.check_request do |_endpoint, data, _headers| + # billing details + assert_match(%r(\n), data) + assert_match(%r(Longbob), data) + assert_match(%r(Longsen), data) + assert_match(%r(221B Baker Street), data) + assert_match(%r(London), data) + assert_match(%r(NW16XE), data) + assert_match(%r(GB), data) + # card details + assert_match(%r(\n), data) + assert_match(%r(4111111111111111), data) + assert_match(%r(#{@gateway.format(@credit_card.month, :two_digits)}), data) + assert_match(%r(#{@gateway.format(@credit_card.year, :four_digits)}), data) + # merchant data + assert_match(%r(test), data) + assert_match(%r(#{@options[:order_id]}), data) + # amount data + assert_match(%r(\n), data) + assert_match(%r(#{@gateway.send(:localized_amount, @amount.to_i, @options[:currency])}), data) + # 3ds exemption tag + assert_match %r(\n), data + assert_match(%r(<#{CyberSourceGateway::THREEDS_EXEMPTIONS[exemption]}>1), data) + end.respond_with(successful_purchase_response) + end + end + + def test_add_stored_credential_3ds_exemption + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(options_with_normalized_3ds, three_ds_exemption_type: CyberSourceGateway::THREEDS_EXEMPTIONS[:stored_credential], merchant_id: 'test', billing_address: { + 'address1' => '221B Baker Street', + 'city' => 'London', + 'zip' => 'NW16XE', + 'country' => 'GB' + })) + end.check_request do |_endpoint, data, _headers| + # billing details + assert_match(%r(\n), data) + assert_match(%r(Longbob), data) + assert_match(%r(Longsen), data) + assert_match(%r(221B Baker Street), data) + assert_match(%r(London), data) + assert_match(%r(NW16XE), data) + assert_match(%r(GB), data) + # card details + assert_match(%r(\n), data) + assert_match(%r(4111111111111111), data) + assert_match(%r(#{@gateway.format(@credit_card.month, :two_digits)}), data) + assert_match(%r(#{@gateway.format(@credit_card.year, :four_digits)}), data) + # merchant data + assert_match(%r(test), data) + assert_match(%r(#{@options[:order_id]}), data) + # amount data + assert_match(%r(\n), data) + assert_match(%r(#{@gateway.send(:localized_amount, @amount.to_i, @options[:currency])}), data) + # 3ds exemption tag + assert_match(%r(true), data) + end.respond_with(successful_purchase_response) + end + def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end @@ -1357,6 +1602,96 @@ def test_invalid_field assert_equal 'c:billTo/c:postalCode', response.params['invalidField'] end + def test_cvv_mismatch_successful_auto_void + @gateway.expects(:ssl_post).returns(cvv_mismatch_response) + @gateway.expects(:void).once.returns(ActiveMerchant::Billing::Response.new(true, 'Transaction successful')) + + response = @gateway.authorize(@amount, credit_card, @options.merge!(auto_void_230: true)) + + assert_failure response + assert_equal '230', response.params['reasonCode'] + assert_equal 'The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the card verification check - transaction has been auto-voided.', response.message + end + + def test_cvv_mismatch + @gateway.expects(:ssl_post).returns(cvv_mismatch_response) + @gateway.expects(:void).never + + response = @gateway.purchase(@amount, credit_card, @options) + + assert_failure response + assert_equal '230', response.params['reasonCode'] + assert_equal 'The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the card verification check', response.message + end + + def test_cvv_mismatch_auto_void_failed + @gateway.expects(:ssl_post).returns(cvv_mismatch_response) + @gateway.expects(:void) + response = @gateway.purchase(@amount, credit_card, @options.merge!(auto_void_230: true)) + + assert_failure response + assert_equal '230', response.params['reasonCode'] + assert_equal 'The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the card verification check - transaction could not be auto-voided.', response.message + end + + def test_able_to_properly_handle_40bytes_cryptogram + long_cryptogram = "NZwc40C4eTDWHVDXPekFaKkNYGk26w+GYDZmU50cATbjqOpNxR/eYA==\n" + credit_card = network_tokenization_credit_card('4111111111111111', brand: 'american_express', payment_cryptogram: long_cryptogram) + + stub_comms do + @gateway.authorize(@amount, credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, body, _headers| + assert_xml_valid_to_xsd(body) + first_half = Base64.encode64(Base64.decode64(long_cryptogram)[0...20]) + second_half = Base64.encode64(Base64.decode64(long_cryptogram)[20...40]) + assert_match %r{#{first_half}}, body + assert_match %r{#{second_half}}, body + end + end + + def test_able_to_properly_handle_20bytes_cryptogram + credit_card = network_tokenization_credit_card('4111111111111111', brand: 'american_express', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + + stub_comms do + @gateway.authorize(@amount, credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, body, _headers| + assert_xml_valid_to_xsd(body) + assert_match %r{#{credit_card.payment_cryptogram}\n}, body + assert_not_match %r{}, body + end + end + + def test_raises_error_on_network_token_with_an_underlying_discover_card + error = assert_raises ArgumentError do + credit_card = network_tokenization_credit_card('4111111111111111', brand: 'discover', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + + @gateway.authorize(100, credit_card, @options) + end + assert_equal 'Payment method discover is not supported, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html', error.message + end + + def test_raises_error_on_network_token_with_an_underlying_apms + error = assert_raises ArgumentError do + credit_card = network_tokenization_credit_card('4111111111111111', brand: 'sodexo', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + + @gateway.authorize(100, credit_card, @options) + end + + assert_equal 'Payment method sodexo is not supported, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html', error.message + end + + def test_routing_number_formatting_with_regular_routing_number + assert_equal @gateway.send(:format_routing_number, '012345678', { currency: 'USD' }), '012345678' + end + + def test_routing_number_formatting_with_canadian_routing_number + assert_equal @gateway.send(:format_routing_number, '12345678', { currency: 'USD' }), '12345678' + end + + def test_routing_number_formatting_with_canadian_routing_number_and_padding + assert_equal @gateway.send(:format_routing_number, '012345678', { currency: 'CAD' }), '12345678' + end + private def options_with_normalized_3ds( @@ -1671,6 +2006,15 @@ def invalid_field_response XML end + def cvv_mismatch_response + <<~XML + + + 2019-09-05T14:10:46.665Z5676926465076767004068REJECT230c:billTo/c:postalCodeAhjzbwSTM78uTleCsJWkEAJRqivRidukDssiQgRm0ky3SA7oegDUiwLm + + XML + end + def invalid_xml_response "What's all this then, govna?

" end diff --git a/test/unit/gateways/d_local_test.rb b/test/unit/gateways/d_local_test.rb index b878fc05fcb..7bed8cc718a 100644 --- a/test/unit/gateways/d_local_test.rb +++ b/test/unit/gateways/d_local_test.rb @@ -22,6 +22,10 @@ def setup } end + def test_supported_countries + assert_equal %w[AR BD BO BR CL CM CN CO CR DO EC EG GH GT IN ID JP KE MY MX MA NG PA PY PE PH SN SV TH TR TZ UG UY VN ZA], DLocalGateway.supported_countries + end + def test_successful_purchase @gateway.expects(:ssl_post).returns(successful_purchase_response) @@ -32,6 +36,14 @@ def test_successful_purchase assert response.test? end + def test_purchase_with_save + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(save: true)) + end.check_request do |_endpoint, data, _headers| + assert_equal true, JSON.parse(data)['card']['save'] + end.respond_with(successful_purchase_response) + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) @@ -52,6 +64,77 @@ def test_purchase_with_installments end.respond_with(successful_purchase_response) end + def test_purchase_with_network_tokens + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + stub_comms do + @gateway.purchase(@amount, credit_card) + end.check_request do |_endpoint, data, _headers| + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', JSON.parse(data)['card']['cryptogram'] + assert_equal '4242424242424242', JSON.parse(data)['card']['network_token'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_network_tokens_and_store_credential_type_subscription + options = @options.merge!(stored_credential: stored_credential(:merchant, :recurring, ntid: 'abc123')) + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + stub_comms do + @gateway.purchase(@amount, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', JSON.parse(data)['card']['cryptogram'] + assert_equal '4242424242424242', JSON.parse(data)['card']['network_token'] + assert_equal 'SUBSCRIPTION', JSON.parse(data)['card']['stored_credential_type'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_network_tokens_and_store_credential_type_uneschedule + options = @options.merge!(stored_credential: stored_credential(:merchant, :unscheduled, ntid: 'abc123')) + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + stub_comms do + @gateway.purchase(@amount, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', JSON.parse(data)['card']['cryptogram'] + assert_equal '4242424242424242', JSON.parse(data)['card']['network_token'] + assert_equal 'UNSCHEDULED_CARD_ON_FILE', JSON.parse(data)['card']['stored_credential_type'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_network_tokens_and_store_credential_usage_first + options = @options.merge!(stored_credential: stored_credential(:cardholder, :initial)) + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + stub_comms do + @gateway.purchase(@amount, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', JSON.parse(data)['card']['cryptogram'] + assert_equal '4242424242424242', JSON.parse(data)['card']['network_token'] + assert_equal 'FIRST', JSON.parse(data)['card']['stored_credential_usage'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_network_tokens_and_store_credential_type_card_on_file_and_credential_usage_used + options = @options.merge!(stored_credential: stored_credential(:cardholder, :unscheduled, ntid: 'abc123')) + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + stub_comms do + @gateway.purchase(@amount, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', JSON.parse(data)['card']['cryptogram'] + assert_equal '4242424242424242', JSON.parse(data)['card']['network_token'] + assert_equal 'CARD_ON_FILE', JSON.parse(data)['card']['stored_credential_type'] + assert_equal 'USED', JSON.parse(data)['card']['stored_credential_usage'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_network_tokens_and_store_credential_usage + options = @options.merge!(stored_credential: stored_credential(:cardholder, :recurring, ntid: 'abc123')) + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + stub_comms do + @gateway.purchase(@amount, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', JSON.parse(data)['card']['cryptogram'] + assert_equal '4242424242424242', JSON.parse(data)['card']['network_token'] + assert_equal 'USED', JSON.parse(data)['card']['stored_credential_usage'] + end.respond_with(successful_purchase_response) + end + def test_successful_purchase_with_additional_data additional_data = { 'submerchant' => { 'name' => 'socks' } } @@ -70,6 +153,14 @@ def test_successful_purchase_with_force_type end.respond_with(successful_purchase_response) end + def test_successful_purchase_with_original_order_id + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(original_order_id: '123ABC')) + end.check_request do |_endpoint, data, _headers| + assert_equal '123ABC', JSON.parse(data)['original_order_id'] + end.respond_with(successful_purchase_response) + end + def test_successful_authorize @gateway.expects(:ssl_post).returns(successful_authorize_response) @@ -119,6 +210,24 @@ def test_passing_nil_address_1 end.respond_with(successful_authorize_response) end + def test_successful_inquire_with_payment_id + stub_comms(@gateway, :ssl_request) do + @gateway.inquire('D-15104-f9e16b85-5fc8-40f0-a4d8-4e73a892594f', {}) + end.check_request do |_method, endpoint, data, _headers| + refute_match(/"https:\/\/sandbox.dlocal.com\/payments\/D-15104-f9e16b85-5fc8-40f0-a4d8-4e73a892594f\/status\/"/, endpoint) + refute_match(nil, data) + end.respond_with(successful_payment_status_response) + end + + def test_successful_inquire_with_order_id + stub_comms(@gateway, :ssl_request) do + @gateway.inquire(nil, { order_id: '62595c5db10fdf7b5d5bb3a16d130992' }) + end.check_request do |_method, endpoint, data, _headers| + refute_match(/"https:\/\/sandbox.dlocal.com\/orders\/62595c5db10fdf7b5d5bb3a16d130992\/"/, endpoint) + refute_match(nil, data) + end.respond_with(successful_orders_response) + end + def test_passing_country_as_string stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, @credit_card, @options) @@ -234,6 +343,15 @@ def test_api_version_param_header end.respond_with(successful_purchase_response) end + def test_idempotency_header + options = @options.merge(idempotency_key: '12345') + stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, _data, headers| + assert_equal '12345', headers['X-Idempotency-Key'] + end.respond_with(successful_purchase_response) + end + def test_three_ds_v1_object_construction post = {} @options[:three_d_secure] = @three_ds_secure @@ -426,6 +544,14 @@ def successful_refund_response '{"id":"REF-15104-a9cc29e5-1895-4cec-94bd-aa16c3b92570","payment_id":"D-15104-f9e16b85-5fc8-40f0-a4d8-4e73a892594f","status":"SUCCESS","currency":"BRL","created_date":"2018-12-06T20:28:37.000+0000","amount":1.00,"status_code":200,"status_detail":"The refund was paid","notification_url":"http://example.com","amount_refunded":1.00,"id_payment":"D-15104-f9e16b85-5fc8-40f0-a4d8-4e73a892594f"}' end + def successful_payment_status_response + '{"code":100,"message":"The payment is pending."}' + end + + def successful_orders_response + '{"order_id":"b809a1aa481b88aaa858144798da656d","payment_id":"T-15104-15f4044d-c4b1-4a38-9b47-bb8be126491d","currency":"BRL","amount":2.0,"created_date":"2022-09-19T13:16:22.000+0000","approved_date":"2022-09-19T13:16:22.000+0000","status":"PAID","status_detail":"The payment was paid.","status_code":"200"}' + end + # I can't invoke a pending response and there is no example in docs, so this response is speculative def pending_refund_response '{"id":"REF-15104-a9cc29e5-1895-4cec-94bd-aa16c3b92570","payment_id":"D-15104-f9e16b85-5fc8-40f0-a4d8-4e73a892594f","status":"PENDING","currency":"BRL","created_date":"2018-12-06T20:28:37.000+0000","amount":1.00,"status_code":100,"status_detail":"The refund is pending","notification_url":"http://example.com","amount_refunded":1.00,"id_payment":"D-15104-f9e16b85-5fc8-40f0-a4d8-4e73a892594f"}' diff --git a/test/unit/gateways/decidir_test.rb b/test/unit/gateways/decidir_test.rb index 7a1626530dc..3c82aada301 100644 --- a/test/unit/gateways/decidir_test.rb +++ b/test/unit/gateways/decidir_test.rb @@ -38,6 +38,13 @@ def setup amount: 1500 } ] + + @network_token = network_tokenization_credit_card( + '4012001037141112', + brand: 'visa', + eci: '05', + payment_cryptogram: '000203016912340000000FA08400317500000000' + ) end def test_successful_purchase @@ -368,6 +375,32 @@ def test_failed_verify_for_without_preauth_mode end end + def test_successful_inquire_with_authorization + @gateway_for_purchase.expects(:ssl_request).returns(successful_inquire_response) + response = @gateway_for_purchase.inquire('818423490') + assert_success response + + assert_equal 544453, response.authorization + assert_equal 'rejected', response.message + assert response.test? + end + + def test_network_token_payment_method + options = { + card_holder_name: 'Tesest payway', + card_holder_door_number: 1234, + card_holder_birthday: '200988', + card_holder_identification_type: 'DNI', + card_holder_identification_number: '44444444', + last_4: @credit_card.last_digits + } + @gateway_for_auth.expects(:ssl_request).returns(successful_network_token_response) + response = @gateway_for_auth.authorize(100, @network_token, options) + + assert_success response + assert_equal 49120515, response.authorization + end + def test_scrub assert @gateway_for_purchase.supports_scrubbing? assert_equal @gateway_for_purchase.scrub(pre_scrubbed), post_scrubbed @@ -539,6 +572,59 @@ def failed_authorize_response ) end + def successful_network_token_response + %( + {"id": 49120515, + "site_transaction_id": "Tx1673372774", + "payment_method_id": 1, + "card_brand": "Visa", + "amount": 1200, + "currency": "ars", + "status": "approved", + "status_details": { + "ticket": "88", + "card_authorization_code": "B45857", + "address_validation_code": "VTE2222", + "error": null + }, + "date": "2023-01-10T14:46Z", + "customer": null, + "bin": "450799", + "installments": 1, + "first_installment_expiration_date": null, + "payment_type": "single", + "sub_payments": [], + "site_id": "09001000", + "fraud_detection": null, + "aggregate_data": { + "indicator": "1", + "identification_number": "30598910045", + "bill_to_pay": "Payway_Test", + "bill_to_refund": "Payway_Test", + "merchant_name": "PAYWAY", + "street": "Lavarden", + "number": "247", + "postal_code": "C1437FBE", + "category": "05044", + "channel": "005", + "geographic_code": "C1437", + "city": "Buenos Aires", + "merchant_id": "id_Aggregator", + "province": "Buenos Aires", + "country": "Argentina", + "merchant_email": "qa@test.com", + "merchant_phone": "+541135211111" + }, + "establishment_name": null, + "spv":null, + "confirmed":null, + "bread":null, + "customer_token":null, + "card_data":"/tokens/49120515", + "token":"b7b6ca89-ed81-44e0-9d1f-3b3cf443cd74"} + ) + end + def successful_capture_response %( {"id":7720214,"site_transaction_id":"0fcedc95-4fbc-4299-80dc-f77e9dd7f525","payment_method_id":1,"card_brand":"Visa","amount":100,"currency":"ars","status":"approved","status_details":{"ticket":"8187","card_authorization_code":"180548","address_validation_code":"VTE0011","error":null},"date":"2019-06-21T18:05Z","customer":null,"bin":"450799","installments":1,"first_installment_expiration_date":null,"payment_type":"single","sub_payments":[],"site_id":"99999997","fraud_detection":null,"aggregate_data":null,"establishment_name":null,"spv":null,"confirmed":{"id":78436,"origin_amount":100,"date":"2019-06-21T03:00Z"},"pan":"345425f15b2c7c4584e0044357b6394d7e","customer_token":null,"card_data":"/tokens/7720214"} @@ -587,6 +673,12 @@ def failed_void_response ) end + def successful_inquire_response + %( + { "id": 544453,"site_transaction_id": "52139443","token": "ef4504fc-21f1-4608-bb75-3f73aa9b9ede","user_id": null,"card_brand": "visa","bin": "483621","amount": 10,"currency": "ars","installments": 1,"description": "","payment_type": "single","sub_payments": [],"status": "rejected","status_details": null,"date": "2016-12-15T15:12Z","merchant_id": null,"fraud_detection": {}} + ) + end + def unique_purchase_error_response %{ {\"error\":{\"error_type\":\"invalid_request_error\",\"validation_errors\":[{\"code\":\"invalid_param\",\"param\":\"payment_type\"}]}} diff --git a/test/unit/gateways/deepstack_test.rb b/test/unit/gateways/deepstack_test.rb new file mode 100644 index 00000000000..c355a5e6a18 --- /dev/null +++ b/test/unit/gateways/deepstack_test.rb @@ -0,0 +1,284 @@ +require 'test_helper' + +class DeepstackTest < Test::Unit::TestCase + def setup + Base.mode = :test + @gateway = DeepstackGateway.new(fixtures(:deepstack)) + @credit_card = credit_card + @amount = 100 + + @credit_card = ActiveMerchant::Billing::CreditCard.new( + number: '4111111111111111', + verification_value: '999', + month: '01', + year: '2029', + first_name: 'Bob', + last_name: 'Bobby' + ) + + address = { + address1: '123 Some st', + address2: '', + first_name: 'Bob', + last_name: 'Bobberson', + city: 'Some City', + state: 'CA', + zip: '12345', + country: 'USA', + email: 'test@test.com' + } + + shipping_address = { + address1: '321 Some st', + address2: '#9', + first_name: 'Jane', + last_name: 'Doe', + city: 'Other City', + state: 'CA', + zip: '12345', + country: 'USA', + phone: '1231231234', + email: 'test@test.com' + } + + @options = { + order_id: '1', + billing_address: address, + shipping_address: shipping_address, + description: 'Store Purchase' + } + end + + def test_successful_token + @gateway.expects(:ssl_post).returns(successful_token_response) + response = @gateway.get_token(@credit_card, @options) + assert_success response + end + + def test_failed_token + @gateway.expects(:ssl_post).returns(failed_token_response) + response = @gateway.get_token(@credit_card, @options) + assert_failure response + end + + def test_successful_purchase + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + assert_equal 'ch_IoSx345fOU6SP67MRXgqWw', response.authorization + assert response.test? + end + + def test_failed_purchase + @gateway.expects(:ssl_post).returns(failed_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + end + + def test_successful_authorize + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert_equal 'ch_vfndMRFdEUac0SnBNAAT6g', response.authorization + end + + def test_failed_authorize + @gateway.expects(:ssl_post).returns(failed_authorize_response) + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_failure response + assert_not_equal 'Approved', response.message + end + + def test_successful_capture + @gateway.expects(:ssl_post).returns(successful_authorize_response) + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + + @gateway.expects(:ssl_post).returns(successful_capture_response) + response = @gateway.capture(@amount, response.authorization) + assert_success response + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(failed_capture_response) + response = @gateway.capture(@amount, '') + + assert_failure response + end + + def test_successful_refund + @gateway.expects(:ssl_post).returns(successful_authorize_response) + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + + @gateway.expects(:ssl_post).returns(successful_refund_response) + response = @gateway.refund(@amount, response.authorization) + assert_success response + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + response = @gateway.refund(@amount, '') + + assert_failure response + end + + def test_successful_void + @gateway.expects(:ssl_post).returns(successful_authorize_response) + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + + @gateway.expects(:ssl_post).returns(successful_void_response) + response = @gateway.void(@amount, response.authorization) + assert_success response + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + response = @gateway.void(@amount, '') + + assert_failure response + end + + def test_successful_verify + @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response) + response = @gateway.verify(@credit_card, @options) + + assert_success response + end + + def test_failed_verify + @gateway.expects(:ssl_request).returns(failed_authorize_response) + response = @gateway.verify(@credit_card, @options) + + assert_failure response + assert_match %r{Invalid Request: Card number is invalid.}, response.message + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def pre_scrubbed + ' + opening connection to api.sandbox.deepstack.io:443... + opened + starting SSL for api.sandbox.deepstack.io:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256 + I, [2023-07-25T08:47:29.985581 #86287] INFO -- : [ActiveMerchant::Billing::DeepstackGateway] connection_ssl_version=TLSv1.2 connection_ssl_cipher=ECDHE-RSA-AES128-GCM-SHA256 + D, [2023-07-25T08:47:29.985687 #86287] DEBUG -- : {"source":{"type":"credit_card","credit_card":{"account_number":"4111111111111111","cvv":"999","expiration":"0129","customer_id":""},"billing_contact":{"first_name":"Bob","last_name":"Bobberson","phone":"1231231234","address":{"line_1":"123 Some st","line_2":"","city":"Some City","state":"CA","postal_code":"12345","country_code":"USA"}}},"transaction":{"amount":100,"cof_type":"UNSCHEDULED_CARDHOLDER","capture":false,"currency_code":"USD","avs":true,"save_payment_instrument":false},"meta":{"shipping_info":{"first_name":"Jane","last_name":"Doe","phone":"1231231234","email":"test@test.com","address":{"line_1":"321 Some st","line_2":"#9","city":"Other City","state":"CA","postal_code":"12345","country_code":"USA"}},"client_transaction_id":"1","client_transaction_description":"Store Purchase"}} + <- "POST /api/v1/payments/charge HTTP/1.1\r\nContent-Type: application/json\r\nAccept: text/plain\r\nHmac: YWZhMjVkZWEtNThlMy00ZGEwLWE1MWUtYmI2ZGNhOTQ5YzkwfFBPU1R8MjAyMy0wNy0yNVQxNTo0NzoyOC43NzZafDIwMmIwZDJjLTdhZWMtNDk2Yy1hMTBlLWQ3ZDUzYTRhNTAzZHxpQmxXTFNNNFdjSjFkSGdlczJYb2JqWUpMVUlGM2tkeUg2b1RFbWtFRUVFPQ==\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: api.sandbox.deepstack.io\r\nContent-Length: 799\r\n\r\n" + <- "{\"source\":{\"type\":\"credit_card\",\"credit_card\":{\"account_number\":\"4111111111111111\",\"cvv\":\"999\",\"expiration\":\"0129\",\"customer_id\":\"\"},\"billing_contact\":{\"first_name\":\"Bob\",\"last_name\":\"Bobberson\",\"phone\":\"1231231234\",\"address\":{\"line_1\":\"123 Some st\",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"}}},\"transaction\":{\"amount\":100,\"cof_type\":\"UNSCHEDULED_CARDHOLDER\",\"capture\":false,\"currency_code\":\"USD\",\"avs\":true,\"save_payment_instrument\":false},\"meta\":{\"shipping_info\":{\"first_name\":\"Jane\",\"last_name\":\"Doe\",\"phone\":\"1231231234\",\"email\":\"test@test.com\",\"address\":{\"line_1\":\"321 Some st\",\"line_2\":\"#9\",\"city\":\"Other City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"}},\"client_transaction_id\":\"1\",\"client_transaction_description\":\"Store Purchase\"}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Tue, 25 Jul 2023 15:47:30 GMT\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Content-Length: 1389\r\n" + -> "Connection: close\r\n" + -> "server: Kestrel\r\n" + -> "apigw-requestid: IoI23jbrPHcESNQ=\r\n" + -> "api-supported-versions: 1.0\r\n" + -> "\r\n" + reading 1389 bytes... + -> "{\"id\":\"ch_gSuF1hGsU0CpPPAUs1dg-Q\",\"response_code\":\"00\",\"message\":\"Approved\",\"approved\":true,\"auth_code\":\"asdefr\",\"cvv_result\":\"Y\",\"avs_result\":\"Y\",\"source\":{\"id\":\"\",\"credit_card\":{\"account_number\":\"************1111\",\"expiration\":\"0129\",\"cvv\":\"999\",\"brand\":\"Visa\",\"last_four\":\"1111\"},\"type\":\"credit_card\",\"client_customer_id\":null,\"billing_contact\":{\"first_name\":\"Bob Bobberson\",\"last_name\":\"\",\"address\":{\"line_1\":\"123 Some st \",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":null}},\"amount\":100,\"captured\":false,\"cof_type\":\"UNSCHEDULED_CARDHOLDER\",\"currency_code\":\"USD\",\"country_code\":0,\"billing_info\":{\"first_name\":\"Bob Bobberson\",\"last_name\":\"\",\"address\":{\"line_1\":\"123 Some st \",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":null},\"shipping_info\":{\"first_name\":\"Jane\",\"last_name\":\"Doe\",\"address\":{\"line_1\":\"321 Some st\",\"line_2\":\"#9\",\"city\":\"Other City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":\"test@test.com\"},\"client_transaction_id\":\"1\",\"client_transaction_description\":\"Store Purchase\",\"client_invoice_id\":null,\"save_payment_instrument\":false,\"kount_score\":null,\"checks\":{\"address_line1_check\":\"pass\",\"address_postal_code_check\":\"pass\",\"cvc_check\":null},\"completed\":\"2023-07-25T15:47:30.183095Z\"}" + read 1389 bytes + Conn close + ' + end + + def post_scrubbed + ' + opening connection to api.sandbox.deepstack.io:443... + opened + starting SSL for api.sandbox.deepstack.io:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256 + I, [2023-07-25T08:47:29.985581 #86287] INFO -- : [ActiveMerchant::Billing::DeepstackGateway] connection_ssl_version=TLSv1.2 connection_ssl_cipher=ECDHE-RSA-AES128-GCM-SHA256 + D, [2023-07-25T08:47:29.985687 #86287] DEBUG -- : {"source":{"type":"credit_card","credit_card":{"account_number":"4111111111111111","cvv":"999","expiration":"0129","customer_id":""},"billing_contact":{"first_name":"Bob","last_name":"Bobberson","phone":"1231231234","address":{"line_1":"123 Some st","line_2":"","city":"Some City","state":"CA","postal_code":"12345","country_code":"USA"}}},"transaction":{"amount":100,"cof_type":"UNSCHEDULED_CARDHOLDER","capture":false,"currency_code":"USD","avs":true,"save_payment_instrument":false},"meta":{"shipping_info":{"first_name":"Jane","last_name":"Doe","phone":"1231231234","email":"test@test.com","address":{"line_1":"321 Some st","line_2":"#9","city":"Other City","state":"CA","postal_code":"12345","country_code":"USA"}},"client_transaction_id":"1","client_transaction_description":"Store Purchase"}} + <- "POST /api/v1/payments/charge HTTP/1.1\r\nContent-Type: application/json\r\nAccept: text/plain\r\nHmac: [FILTERED]\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: api.sandbox.deepstack.io\r\nContent-Length: 799\r\n\r\n" + <- "{\"source\":{\"type\":\"credit_card\",\"credit_card\":{\"account_number\":\"[FILTERED]\",\"cvv\":\"[FILTERED]\",\"expiration\":\"[FILTERED]\",\"customer_id\":\"\"},\"billing_contact\":{\"first_name\":\"Bob\",\"last_name\":\"Bobberson\",\"phone\":\"1231231234\",\"address\":{\"line_1\":\"123 Some st\",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"}}},\"transaction\":{\"amount\":100,\"cof_type\":\"UNSCHEDULED_CARDHOLDER\",\"capture\":false,\"currency_code\":\"USD\",\"avs\":true,\"save_payment_instrument\":false},\"meta\":{\"shipping_info\":{\"first_name\":\"Jane\",\"last_name\":\"Doe\",\"phone\":\"1231231234\",\"email\":\"test@test.com\",\"address\":{\"line_1\":\"321 Some st\",\"line_2\":\"#9\",\"city\":\"Other City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"}},\"client_transaction_id\":\"1\",\"client_transaction_description\":\"Store Purchase\"}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Tue, 25 Jul 2023 15:47:30 GMT\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Content-Length: 1389\r\n" + -> "Connection: close\r\n" + -> "server: Kestrel\r\n" + -> "apigw-requestid: IoI23jbrPHcESNQ=\r\n" + -> "api-supported-versions: 1.0\r\n" + -> "\r\n" + reading 1389 bytes... + -> "{\"id\":\"ch_gSuF1hGsU0CpPPAUs1dg-Q\",\"response_code\":\"00\",\"message\":\"Approved\",\"approved\":true,\"auth_code\":\"asdefr\",\"cvv_result\":\"Y\",\"avs_result\":\"Y\",\"source\":{\"id\":\"\",\"credit_card\":{\"account_number\":\"[FILTERED]\",\"expiration\":\"[FILTERED]\",\"cvv\":\"[FILTERED]\",\"brand\":\"Visa\",\"last_four\":\"1111\"},\"type\":\"credit_card\",\"client_customer_id\":null,\"billing_contact\":{\"first_name\":\"Bob Bobberson\",\"last_name\":\"\",\"address\":{\"line_1\":\"123 Some st \",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":null}},\"amount\":100,\"captured\":false,\"cof_type\":\"UNSCHEDULED_CARDHOLDER\",\"currency_code\":\"USD\",\"country_code\":0,\"billing_info\":{\"first_name\":\"Bob Bobberson\",\"last_name\":\"\",\"address\":{\"line_1\":\"123 Some st \",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":null},\"shipping_info\":{\"first_name\":\"Jane\",\"last_name\":\"Doe\",\"address\":{\"line_1\":\"321 Some st\",\"line_2\":\"#9\",\"city\":\"Other City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":\"test@test.com\"},\"client_transaction_id\":\"1\",\"client_transaction_description\":\"Store Purchase\",\"client_invoice_id\":null,\"save_payment_instrument\":false,\"kount_score\":null,\"checks\":{\"address_line1_check\":\"pass\",\"address_postal_code_check\":\"pass\",\"cvc_check\":null},\"completed\":\"2023-07-25T15:47:30.183095Z\"}" + read 1389 bytes + Conn close + ' + end + + def successful_purchase_response_2 + %( + Easy to capture by setting the DEBUG_ACTIVE_MERCHANT environment variable + to "true" when running remote tests: + + $ DEBUG_ACTIVE_MERCHANT=true ruby -Itest \ + test/remote/gateways/remote_deepstack_test.rb \ + -n test_successful_purchase + ) + end + + def successful_purchase_response + %({\"id\":\"ch_IoSx345fOU6SP67MRXgqWw\",\"response_code\":\"00\",\"message\":\"Approved\",\"approved\":true,\"auth_code\":\"asdefr\",\"cvv_result\":\"Y\",\"avs_result\":\"Y\",\"source\":{\"id\":\"\",\"credit_card\":{\"account_number\":\"************1111\",\"expiration\":\"0129\",\"cvv\":\"999\",\"brand\":\"Visa\",\"last_four\":\"1111\"},\"type\":\"credit_card\",\"client_customer_id\":null,\"billing_contact\":{\"first_name\":\"Bob Bobberson\",\"last_name\":\"\",\"address\":{\"line_1\":\"123 Some st \",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":\"test@test.com\"}},\"amount\":100,\"captured\":true,\"cof_type\":\"UNSCHEDULED_CARDHOLDER\",\"currency_code\":\"USD\",\"country_code\":0,\"billing_info\":{\"first_name\":\"Bob Bobberson\",\"last_name\":\"\",\"address\":{\"line_1\":\"123 Some st \",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":\"test@test.com\"},\"client_transaction_id\":\"1\",\"client_transaction_description\":\"Store Purchase\",\"client_invoice_id\":null,\"save_payment_instrument\":false,\"kount_score\":null,\"checks\":{\"address_line1_check\":\"pass\",\"address_postal_code_check\":\"pass\",\"cvc_check\":null},\"completed\":\"2023-07-14T17:08:33.5004521Z\"}) + end + + def failed_purchase_response + %({\"id\":\"ch_xbaPjifXN0Gum4vzdup6iA\",\"response_code\":\"03\",\"message\":\"Invalid Request: Card number is invalid.\",\"approved\":false,\"source\":{\"id\":\"\",\"credit_card\":{\"account_number\":\"************0051\",\"expiration\":\"0129\",\"cvv\":\"999\",\"brand\":\"MasterCard\",\"last_four\":\"0051\"},\"type\":\"credit_card\",\"client_customer_id\":null,\"billing_contact\":{\"first_name\":\"Bob Bobberson\",\"last_name\":\"\",\"address\":{\"line_1\":\"123 Some st \",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":\"test@test.com\"}},\"amount\":100,\"captured\":false,\"cof_type\":\"UNSCHEDULED_CARDHOLDER\",\"currency_code\":\"USD\",\"country_code\":0,\"billing_info\":{\"first_name\":\"Bob Bobberson\",\"last_name\":\"\",\"address\":{\"line_1\":\"123 Some st \",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":\"test@test.com\"},\"client_transaction_id\":\"1\",\"client_transaction_description\":\"Store Purchase\",\"client_invoice_id\":null,\"save_payment_instrument\":false,\"kount_score\":null,\"checks\":{\"address_line1_check\":null,\"address_postal_code_check\":null,\"cvc_check\":null},\"completed\":\"2023-07-14T17:11:24.972201Z\"}) + end + + def successful_authorize_response + %({\"id\":\"ch_vfndMRFdEUac0SnBNAAT6g\",\"response_code\":\"00\",\"message\":\"Approved\",\"approved\":true,\"auth_code\":\"asdefr\",\"cvv_result\":\"Y\",\"avs_result\":\"Y\",\"source\":{\"id\":\"\",\"credit_card\":{\"account_number\":\"************1111\",\"expiration\":\"0129\",\"cvv\":\"999\",\"brand\":\"Visa\",\"last_four\":\"1111\"},\"type\":\"credit_card\",\"client_customer_id\":null,\"billing_contact\":{\"first_name\":\"Bob Bobberson\",\"last_name\":\"\",\"address\":{\"line_1\":\"123 Some st \",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":\"test@test.com\"}},\"amount\":100,\"captured\":false,\"cof_type\":\"UNSCHEDULED_CARDHOLDER\",\"currency_code\":\"USD\",\"country_code\":0,\"billing_info\":{\"first_name\":\"Bob Bobberson\",\"last_name\":\"\",\"address\":{\"line_1\":\"123 Some st \",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":\"test@test.com\"},\"client_transaction_id\":\"1\",\"client_transaction_description\":\"Store Purchase\",\"client_invoice_id\":null,\"save_payment_instrument\":false,\"kount_score\":null,\"checks\":{\"address_line1_check\":\"pass\",\"address_postal_code_check\":\"pass\",\"cvc_check\":null},\"completed\":\"2023-07-14T17:36:18.4817926Z\"}) + end + + def failed_authorize_response + %({\"id\":\"ch_CBue2iT3pUibJ7QySysTrA\",\"response_code\":\"03\",\"message\":\"Invalid Request: Card number is invalid.\",\"approved\":false,\"source\":{\"id\":\"\",\"credit_card\":{\"account_number\":\"************0051\",\"expiration\":\"0129\",\"cvv\":\"999\",\"brand\":\"MasterCard\",\"last_four\":\"0051\"},\"type\":\"credit_card\",\"client_customer_id\":null,\"billing_contact\":{\"first_name\":\"Bob Bobberson\",\"last_name\":\"\",\"address\":{\"line_1\":\"123 Some st \",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":\"test@test.com\"}},\"amount\":100,\"captured\":false,\"cof_type\":\"UNSCHEDULED_CARDHOLDER\",\"currency_code\":\"USD\",\"country_code\":0,\"billing_info\":{\"first_name\":\"Bob Bobberson\",\"last_name\":\"\",\"address\":{\"line_1\":\"123 Some st \",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":\"test@test.com\"},\"client_transaction_id\":\"1\",\"client_transaction_description\":\"Store Purchase\",\"client_invoice_id\":null,\"save_payment_instrument\":false,\"kount_score\":null,\"checks\":{\"address_line1_check\":null,\"address_postal_code_check\":null,\"cvc_check\":null},\"completed\":\"2023-07-14T17:42:30.1835831Z\"}) + end + + def successful_capture_response + %({\"response_code\":\"00\",\"message\":\"Approved\",\"approved\":true,\"auth_code\":\"asdefr\",\"charge_transaction_id\":\"ch_KpmspGEiSUCgavxiE-xPTw\",\"amount\":100,\"recurring\":false,\"completed\":\"2023-07-14T19:58:49.3255779+00:00\"}) + end + + def failed_capture_response + %({\"response_code\":\"02\",\"message\":\"Current transaction does not exist or is in an invalid state.\",\"approved\":false,\"charge_transaction_id\":\"\",\"amount\":100,\"recurring\":false,\"completed\":\"2023-07-14T21:33:54.2518371Z\"}) + end + + def successful_refund_response + %({\"response_code\":\"00\",\"message\":\"Approved\",\"approved\":true,\"auth_code\":\"asdefr\",\"charge_transaction_id\":\"ch_w5A8LS3C1kqdtrCJxWeRqQ\",\"amount\":10000,\"completed\":\"2023-07-15T01:01:58.3190631+00:00\"}) + end + + def failed_refund_response + %({\"type\":\"https://httpstatuses.com/400\",\"title\":\"Invalid Request\",\"status\":400,\"detail\":\"Specified transaction does not exist.\",\"traceId\":\"00-e9b47344b951b400c34ce541a22e96a7-5ece5267ae02ef3d-00\"}) + end + + def successful_void_response + %({\"response_code\":\"00\",\"message\":\"Approved\",\"approved\":true,\"auth_code\":\"asdefr\",\"charge_transaction_id\":\"ch_w5A8LS3C1kqdtrCJxWeRqQ\",\"amount\":10000,\"completed\":\"2023-07-15T01:01:58.3190631+00:00\"}) + end + + def failed_void_response + %({\"type\":\"https://httpstatuses.com/400\",\"title\":\"Invalid Request\",\"status\":400,\"detail\":\"Specified transaction does not exist.\",\"traceId\":\"00-e9b47344b951b400c34ce541a22e96a7-5ece5267ae02ef3d-00\"}) + end + + def successful_token_response + %({\"id\":\"tok_Ub1AHj7x1U6cUF8x8KDKAw\",\"type\":\"credit_card\",\"customer_id\":null,\"brand\":\"Visa\",\"bin\":\"411111\",\"last_four\":\"1111\",\"expiration\":\"0129\",\"billing_contact\":{\"first_name\":\"Bob\",\"last_name\":\"Bobberson\",\"address\":{\"line_1\":\"123 Some st\",\"line_2\":\"\",\"city\":\"Some City\",\"state\":\"CA\",\"postal_code\":\"12345\",\"country_code\":\"USA\"},\"phone\":\"1231231234\",\"email\":\"test@test.com\"},\"is_default\":false}) + end + + def failed_token_response + %({\"id\":\"Ji-YEeijmUmiFB6mz_iIUA\",\"response_code\":\"400\",\"message\":\"InvalidRequestException: Card number is invalid.\",\"approved\":false,\"completed\":\"2023-07-15T01:10:47.9188024Z\",\"success\":false}) + end +end diff --git a/test/unit/gateways/ebanx_test.rb b/test/unit/gateways/ebanx_test.rb index 7323c6cd1ba..423a1c0f83f 100644 --- a/test/unit/gateways/ebanx_test.rb +++ b/test/unit/gateways/ebanx_test.rb @@ -28,7 +28,7 @@ def test_successful_purchase def test_successful_purchase_with_optional_processing_type_header response = stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@accepted_amount, @credit_card, @options.merge(processing_type: 'local')) + @gateway.purchase(@amount, @credit_card, @options.merge(processing_type: 'local')) end.check_request do |_method, _endpoint, _data, headers| assert_equal 'local', headers['x-ebanx-api-processing-type'] end.respond_with(successful_purchase_response) @@ -36,6 +36,16 @@ def test_successful_purchase_with_optional_processing_type_header assert_success response end + def test_successful_purchase_with_soft_descriptor + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(soft_descriptor: 'ActiveMerchant')) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"soft_descriptor\":\"ActiveMerchant\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_failed_purchase @gateway.expects(:ssl_request).returns(failed_purchase_response) @@ -126,15 +136,7 @@ def test_failed_void end def test_successful_verify - @gateway.expects(:ssl_request).times(2).returns(successful_authorize_response, successful_void_response) - - response = @gateway.verify(@credit_card, @options) - assert_success response - assert_equal nil, response.error_code - end - - def test_successful_verify_with_failed_void - @gateway.expects(:ssl_request).times(2).returns(successful_authorize_response, failed_void_response) + @gateway.expects(:ssl_request).returns(successful_verify_response) response = @gateway.verify(@credit_card, @options) assert_success response @@ -142,11 +144,11 @@ def test_successful_verify_with_failed_void end def test_failed_verify - @gateway.expects(:ssl_request).returns(failed_authorize_response) + @gateway.expects(:ssl_request).returns(failed_verify_response) response = @gateway.verify(@credit_card, @options) assert_failure response - assert_equal 'NOK', response.error_code + assert_equal 'Not accepted', response.message end def test_successful_store_and_purchase @@ -162,6 +164,18 @@ def test_successful_store_and_purchase assert_success response end + def test_successful_purchase_and_inquire + @gateway.expects(:ssl_request).returns(successful_purchase_response) + + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + @gateway.expects(:ssl_request).returns(successful_purchase_response) + response = @gateway.inquire(purchase.authorization) + + assert_success response + end + def test_error_response_with_invalid_creds @gateway.expects(:ssl_request).returns(invalid_cred_response) @@ -213,6 +227,18 @@ def failed_authorize_response ) end + def successful_verify_response + %( + {"status":"SUCCESS","payment_type_code":"creditcard","card_verification":{"transaction_status":{"code":"OK","description":"Accepted"},"transaction_type":"ZERO DOLLAR"}} + ) + end + + def failed_verify_response + %( + {"status":"SUCCESS","payment_type_code":"discover","card_verification":{"transaction_status":{"code":"NOK", "description":"Not accepted"}, "transaction_type":"GHOST AUTHORIZATION"}} + ) + end + def successful_capture_response %( {"payment":{"hash":"5dee94502bd59660b801c441ad5a703f2c4123f5fc892ccb","pin":"675968133","country":"br","merchant_payment_code":"b98b2892b80771b9dadf2ebc482cb65d","order_number":null,"status":"CO","status_date":"2019-12-09 18:37:05","open_date":"2019-12-09 18:37:04","confirm_date":"2019-12-09 18:37:05","transfer_date":null,"amount_br":"4.19","amount_ext":"1.00","amount_iof":"0.02","currency_rate":"4.1700","currency_ext":"USD","due_date":"2019-12-12","instalments":"1","payment_type_code":"visa","details":{"billing_descriptor":"DEMONSTRATION"},"transaction_status":{"acquirer":"EBANX","code":"OK","description":"Accepted"},"pre_approved":true,"capture_available":false,"customer":{"document":"85351346893","email":"unspecified@example.com","name":"LONGBOB LONGSEN","birth_date":null}},"status":"SUCCESS"} diff --git a/test/unit/gateways/element_test.rb b/test/unit/gateways/element_test.rb index 0c273addd1a..bf461d6704a 100644 --- a/test/unit/gateways/element_test.rb +++ b/test/unit/gateways/element_test.rb @@ -25,6 +25,18 @@ def test_successful_purchase assert_equal '2005831886|100', response.authorization end + def test_successful_purchase_without_name + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + @credit_card.first_name = nil + @credit_card.last_name = nil + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + assert_equal '2005831886|100', response.authorization + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) @@ -296,6 +308,16 @@ def test_successful_purchase_with_merchant_descriptor assert_success response end + def test_successful_purchase_with_billing_email + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(email: 'test@example.com')) + end.check_request do |_endpoint, data, _headers| + assert_match 'test@example.com', data + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_successful_credit_with_extra_fields credit_options = @options.merge({ ticket_number: '1', market_code: 'FoodRestaurant', merchant_supplied_transaction_id: '123' }) stub_comms do diff --git a/test/unit/gateways/epay_test.rb b/test/unit/gateways/epay_test.rb index cfebf4b8447..91a03ed50e5 100644 --- a/test/unit/gateways/epay_test.rb +++ b/test/unit/gateways/epay_test.rb @@ -26,8 +26,7 @@ def test_failed_purchase assert response = @gateway.authorize(100, @credit_card) assert_failure response - assert_equal 'The payment was declined. Try again in a moment or try with another credit card.', - response.message + assert_equal 'The payment was declined. Try again in a moment or try with another credit card.', response.message end def test_successful_3ds_purchase @@ -51,8 +50,7 @@ def test_invalid_characters_in_response assert response = @gateway.authorize(100, @credit_card) assert_failure response - assert_equal 'The payment was declined of unknown reasons. For more information contact the bank. E.g. try with another credit card.
Denied - Call your bank for information', - response.message + assert_equal 'The payment was declined of unknown reasons. For more information contact the bank. E.g. try with another credit card.
Denied - Call your bank for information', response.message end def test_failed_response_on_purchase diff --git a/test/unit/gateways/eway_rapid_test.rb b/test/unit/gateways/eway_rapid_test.rb index 4ed739ca9a4..ec70917a51a 100644 --- a/test/unit/gateways/eway_rapid_test.rb +++ b/test/unit/gateways/eway_rapid_test.rb @@ -194,7 +194,9 @@ def test_failed_purchase_with_multiple_messages def test_purchase_with_all_options response = stub_comms do - @gateway.purchase(200, @credit_card, + @gateway.purchase( + 200, + @credit_card, transaction_type: 'CustomTransactionType', redirect_url: 'http://awesomesauce.com', ip: '0.0.0.0', @@ -230,7 +232,8 @@ def test_purchase_with_all_options country: 'US', phone: '1115555555', fax: '1115556666' - }) + } + ) end.check_request do |_endpoint, data, _headers| assert_match(%r{"TransactionType":"CustomTransactionType"}, data) assert_match(%r{"RedirectUrl":"http://awesomesauce.com"}, data) diff --git a/test/unit/gateways/exact_test.rb b/test/unit/gateways/exact_test.rb index 5a1257add89..2986777bfa6 100644 --- a/test/unit/gateways/exact_test.rb +++ b/test/unit/gateways/exact_test.rb @@ -49,9 +49,10 @@ def test_failed_purchase end def test_expdate - assert_equal('%02d%s' % [@credit_card.month, - @credit_card.year.to_s[-2..-1]], - @gateway.send(:expdate, @credit_card)) + assert_equal( + '%02d%s' % [@credit_card.month, @credit_card.year.to_s[-2..-1]], + @gateway.send(:expdate, @credit_card) + ) end def test_soap_fault diff --git a/test/unit/gateways/firstdata_e4_test.rb b/test/unit/gateways/firstdata_e4_test.rb index 21a0a0da180..c18b5941cc9 100755 --- a/test/unit/gateways/firstdata_e4_test.rb +++ b/test/unit/gateways/firstdata_e4_test.rb @@ -63,8 +63,7 @@ def test_successful_purchase_with_token def test_successful_purchase_with_specified_currency_and_token options_with_specified_currency = @options.merge({ currency: 'GBP' }) @gateway.expects(:ssl_post).returns(successful_purchase_with_specified_currency_response) - assert response = @gateway.purchase(@amount, '8938737759041111;visa;Longbob;Longsen;9;2014', - options_with_specified_currency) + assert response = @gateway.purchase(@amount, '8938737759041111;visa;Longbob;Longsen;9;2014', options_with_specified_currency) assert_success response assert_equal 'GBP', response.params['currency'] end @@ -1049,7 +1048,7 @@ def no_transaction_response read: true socket: RESPONSE - YAML.safe_load(yamlexcep, ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) end def bad_credentials_response @@ -1086,7 +1085,7 @@ def bad_credentials_response http_version: '1.1' socket: RESPONSE - YAML.safe_load(yamlexcep, ['Net::HTTPUnauthorized', 'ActiveMerchant::ResponseError']) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPUnauthorized', 'ActiveMerchant::ResponseError']) end def successful_void_response diff --git a/test/unit/gateways/firstdata_e4_v27_test.rb b/test/unit/gateways/firstdata_e4_v27_test.rb index 02f982bb551..a4301598f65 100644 --- a/test/unit/gateways/firstdata_e4_v27_test.rb +++ b/test/unit/gateways/firstdata_e4_v27_test.rb @@ -1001,7 +1001,7 @@ def no_transaction_response read: true socket: RESPONSE - YAML.safe_load(yamlexcep, ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) end def bad_credentials_response @@ -1038,7 +1038,7 @@ def bad_credentials_response http_version: '1.1' socket: RESPONSE - YAML.safe_load(yamlexcep, ['Net::HTTPUnauthorized', 'ActiveMerchant::ResponseError']) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPUnauthorized', 'ActiveMerchant::ResponseError']) end def successful_void_response diff --git a/test/unit/gateways/garanti_test.rb b/test/unit/gateways/garanti_test.rb index cc3416e0f89..ad8ed645b9c 100644 --- a/test/unit/gateways/garanti_test.rb +++ b/test/unit/gateways/garanti_test.rb @@ -9,7 +9,7 @@ def setup Base.mode = :test @gateway = GarantiGateway.new(login: 'a', password: 'b', terminal_id: 'c', merchant_id: 'd') - @credit_card = credit_card(4242424242424242) + @credit_card = credit_card('4242424242424242') @amount = 1000 # 1000 cents, 10$ @options = { diff --git a/test/unit/gateways/gateway_test.rb b/test/unit/gateways/gateway_test.rb index 68d6d0a441e..27ccbd14215 100644 --- a/test/unit/gateways/gateway_test.rb +++ b/test/unit/gateways/gateway_test.rb @@ -28,8 +28,7 @@ def test_should_validate_supported_countries assert_nothing_raised do Gateway.supported_countries = all_country_codes - assert Gateway.supported_countries == all_country_codes, - 'List of supported countries not properly set' + assert Gateway.supported_countries == all_country_codes, 'List of supported countries not properly set' end end diff --git a/test/unit/gateways/global_collect_test.rb b/test/unit/gateways/global_collect_test.rb index 30234be3da3..537d233f5cc 100644 --- a/test/unit/gateways/global_collect_test.rb +++ b/test/unit/gateways/global_collect_test.rb @@ -9,6 +9,33 @@ def setup secret_api_key: '109H/288H*50Y18W4/0G8571F245KA=') @credit_card = credit_card('4567350000427977') + @apple_pay_network_token = network_tokenization_credit_card( + '4444333322221111', + month: 10, + year: 24, + first_name: 'John', + last_name: 'Smith', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :apple_pay + ) + + @google_pay_network_token = network_tokenization_credit_card( + '4444333322221111', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: Time.new.year + 2, + source: :google_pay, + transaction_id: '123456789', + eci: '05' + ) + + @google_pay_pan_only = credit_card( + '4444333322221111', + month: '01', + year: Time.new.year + 2 + ) + @declined_card = credit_card('5424180279791732') @accepted_amount = 4005 @rejected_amount = 2997 @@ -25,22 +52,23 @@ def setup ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', acs_transaction_id: '13c701a3-5a88-4c45-89e9-ef65e50a8bf9', cavv_algorithm: 1, - authentication_response_status: 'Y' + authentication_response_status: 'Y', + flow: 'frictionless' } ) end def test_successful_authorize_and_capture - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, @options) end.respond_with(successful_authorize_response) assert_success response assert_equal '000000142800000000920000100001', response.authorization - capture = stub_comms do + capture = stub_comms(@gateway, :ssl_request) do @gateway.capture(@accepted_amount, response.authorization) - end.check_request do |endpoint, _data, _headers| + end.check_request do |_method, endpoint, _data, _headers| assert_match(/000000142800000000920000100001/, endpoint) end.respond_with(successful_capture_response) @@ -55,9 +83,9 @@ def test_successful_preproduction_url url_override: 'preproduction' ) - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card) - end.check_request do |endpoint, _data, _headers| + end.check_request do |_method, endpoint, _data, _headers| assert_match(/world\.preprod\.api-ingenico\.com\/v1\/#{@gateway.options[:merchant_id]}/, endpoint) end.respond_with(successful_authorize_response) end @@ -65,17 +93,140 @@ def test_successful_preproduction_url # When requires_approval is true (or not present), # a `purchase` makes two calls (`auth` and `capture`). def test_successful_purchase_with_requires_approval_true - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, @options.merge(requires_approval: true)) - end.check_request do |endpoint, data, headers| + end.check_request do |_method, _endpoint, _data, _headers| end.respond_with(successful_authorize_response, successful_capture_response) end + def test_purchase_request_with_google_pay + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @google_pay_network_token) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + assert_equal '320', JSON.parse(data)['mobilePaymentMethodSpecificInput']['paymentProductId'] + end + end + + def test_purchase_request_with_google_pay_pan_only + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @google_pay_pan_only, @options.merge(customer: 'GP1234ID', google_pay_pan_only: true)) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + assert_equal '320', JSON.parse(data)['mobilePaymentMethodSpecificInput']['paymentProductId'] + end + end + + def test_add_payment_for_credit_card + post = {} + options = {} + payment = @credit_card + @gateway.send('add_payment', post, payment, options) + assert_includes post.keys, 'cardPaymentMethodSpecificInput' + assert_equal post['cardPaymentMethodSpecificInput']['paymentProductId'], '1' + assert_equal post['cardPaymentMethodSpecificInput']['authorizationMode'], 'FINAL_AUTHORIZATION' + assert_includes post['cardPaymentMethodSpecificInput'].keys, 'card' + assert_equal post['cardPaymentMethodSpecificInput']['card']['cvv'], '123' + assert_equal post['cardPaymentMethodSpecificInput']['card']['cardNumber'], '4567350000427977' + end + + def test_add_payment_for_google_pay + post = {} + options = {} + payment = @google_pay_network_token + @gateway.send('add_payment', post, payment, options) + assert_includes post.keys.first, 'mobilePaymentMethodSpecificInput' + assert_equal post['mobilePaymentMethodSpecificInput']['paymentProductId'], '320' + assert_equal post['mobilePaymentMethodSpecificInput']['authorizationMode'], 'FINAL_AUTHORIZATION' + assert_includes post['mobilePaymentMethodSpecificInput'].keys, 'decryptedPaymentData' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['dpan'], '4444333322221111' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['cryptogram'], 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['eci'], '05' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['expiryDate'], "01#{payment.year.to_s[-2..-1]}" + assert_equal 'TOKENIZED_CARD', post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['paymentMethod'] + end + + def test_add_payment_for_google_pay_pan_only + post = {} + options = { google_pay_pan_only: true } + payment = @google_pay_pan_only + @gateway.send('add_payment', post, payment, options) + assert_includes post.keys.first, 'mobilePaymentMethodSpecificInput' + assert_equal post['mobilePaymentMethodSpecificInput']['paymentProductId'], '320' + assert_equal post['mobilePaymentMethodSpecificInput']['authorizationMode'], 'FINAL_AUTHORIZATION' + assert_includes post['mobilePaymentMethodSpecificInput'].keys, 'decryptedPaymentData' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['pan'], '4444333322221111' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['expiryDate'], "01#{payment.year.to_s[-2..-1]}" + assert_equal 'CARD', post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['paymentMethod'] + end + + def test_add_payment_for_apple_pay + post = {} + options = {} + payment = @apple_pay_network_token + @gateway.send('add_payment', post, payment, options) + assert_includes post.keys, 'mobilePaymentMethodSpecificInput' + assert_equal post['mobilePaymentMethodSpecificInput']['paymentProductId'], '302' + assert_equal post['mobilePaymentMethodSpecificInput']['authorizationMode'], 'FINAL_AUTHORIZATION' + assert_includes post['mobilePaymentMethodSpecificInput'].keys, 'decryptedPaymentData' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['dpan'], '4444333322221111' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['cryptogram'], 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['eci'], '05' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['expiryDate'], '1024' + end + + def test_add_decrypted_data_google_pay_pan_only + post = { 'mobilePaymentMethodSpecificInput' => {} } + payment = @google_pay_pan_only + options = { google_pay_pan_only: true } + expirydate = '0124' + + @gateway.send('add_decrypted_payment_data', post, payment, options, expirydate) + assert_includes post['mobilePaymentMethodSpecificInput'].keys, 'decryptedPaymentData' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['pan'], '4444333322221111' + assert_equal 'CARD', post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['paymentMethod'] + end + + def test_add_decrypted_data_for_google_pay + post = { 'mobilePaymentMethodSpecificInput' => {} } + payment = @google_pay_network_token + options = {} + expirydate = '0124' + + @gateway.send('add_decrypted_payment_data', post, payment, options, expirydate) + assert_includes post['mobilePaymentMethodSpecificInput'].keys, 'decryptedPaymentData' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['cryptogram'], 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['eci'], '05' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['dpan'], '4444333322221111' + assert_equal 'TOKENIZED_CARD', post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['paymentMethod'] + assert_equal '0124', post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['expiryDate'] + end + + def test_add_decrypted_data_for_apple_pay + post = { 'mobilePaymentMethodSpecificInput' => {} } + payment = @google_pay_network_token + options = {} + expirydate = '0124' + + @gateway.send('add_decrypted_payment_data', post, payment, options, expirydate) + assert_includes post['mobilePaymentMethodSpecificInput'].keys, 'decryptedPaymentData' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['cryptogram'], 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['eci'], '05' + assert_equal post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['dpan'], '4444333322221111' + assert_equal '0124', post['mobilePaymentMethodSpecificInput']['decryptedPaymentData']['expiryDate'] + end + + def test_purchase_request_with_apple_pay + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @apple_pay_network_token) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + assert_equal '302', JSON.parse(data)['mobilePaymentMethodSpecificInput']['paymentProductId'] + end + end + # When requires_approval is false, a `purchase` makes one call (`auth`). def test_successful_purchase_with_requires_approval_false - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, @options.merge(requires_approval: false)) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_equal false, JSON.parse(data)['cardPaymentMethodSpecificInput']['requiresApproval'] end.respond_with(successful_authorize_response) end @@ -87,6 +238,7 @@ def test_successful_purchase_airline_fields name: 'Spreedly Airlines', flight_date: '20190810', passenger_name: 'Randi Smith', + agent_numeric_code: '12345', flight_legs: [ { arrival_airport: 'BDL', origin_airport: 'RDU', @@ -103,9 +255,9 @@ def test_successful_purchase_airline_fields ] } ) - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_equal 111, JSON.parse(data)['order']['additionalInput']['airlineData']['code'] assert_equal '20190810', JSON.parse(data)['order']['additionalInput']['airlineData']['flightDate'] assert_equal 2, JSON.parse(data)['order']['additionalInput']['airlineData']['flightLegs'].length @@ -146,9 +298,9 @@ def test_successful_purchase_lodging_fields ] } ) - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_equal 'advancedDeposit', JSON.parse(data)['order']['additionalInput']['lodgingData']['programCode'] assert_equal '20211223', JSON.parse(data)['order']['additionalInput']['lodgingData']['checkInDate'] assert_equal '1000', JSON.parse(data)['order']['additionalInput']['lodgingData']['charges'][0]['chargeAmount'] @@ -170,24 +322,24 @@ def test_successful_purchase_passenger_fields ] } ) - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_equal 'Julia', JSON.parse(data)['order']['additionalInput']['airlineData']['passengers'][1]['firstName'] assert_equal 2, JSON.parse(data)['order']['additionalInput']['airlineData']['passengers'].length end.respond_with(successful_authorize_response, successful_capture_response) end def test_purchase_passes_installments - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, @options.merge(number_of_installments: '3')) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/"numberOfInstallments\":\"3\"/, data) end.respond_with(successful_authorize_response, successful_capture_response) end def test_purchase_does_not_run_capture_if_authorize_auto_captured - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, @options) end.respond_with(successful_capture_response) @@ -197,9 +349,9 @@ def test_purchase_does_not_run_capture_if_authorize_auto_captured end def test_authorize_with_pre_authorization_flag - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, @options.merge(pre_authorization: true)) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/PRE_AUTHORIZATION/, data) end.respond_with(successful_authorize_response_with_pre_authorization_flag) @@ -207,9 +359,9 @@ def test_authorize_with_pre_authorization_flag end def test_authorize_without_pre_authorization_flag - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, @options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/FINAL_AUTHORIZATION/, data) end.respond_with(successful_authorize_response) @@ -227,26 +379,29 @@ def test_successful_authorization_with_extra_options { 'website' => 'www.example.com', 'giftMessage' => 'Happy Day!' - } + }, + payment_product_id: '123ABC' } ) - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match %r("fraudFields":{"website":"www.example.com","giftMessage":"Happy Day!","customerIpAddress":"127.0.0.1"}), data assert_match %r("merchantReference":"123"), data - assert_match %r("customer":{"personalInformation":{"name":{"firstName":"Longbob","surname":"Longsen"}},"merchantCustomerId":"123987","contactDetails":{"emailAddress":"example@example.com","phoneNumber":"\(555\)555-5555"},"billingAddress":{"street":"456 My Street","additionalInfo":"Apt 1","zip":"K1C2N6","city":"Ottawa","state":"ON","countryCode":"CA"}}}), data + assert_match %r("customer":{"personalInformation":{"name":{"firstName":"Longbob","surname":"Longsen"}},"merchantCustomerId":"123987","contactDetails":{"emailAddress":"example@example.com","phoneNumber":"\(555\)555-5555"},"billingAddress":{"street":"My Street","houseNumber":"456","additionalInfo":"Apt 1","zip":"K1C2N6","city":"Ottawa","state":"ON","countryCode":"CA"}}}), data + assert_match %r("paymentProductId":"123ABC"), data end.respond_with(successful_authorize_response) assert_success response end def test_successful_authorize_with_3ds_auth - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, @options_3ds2) - end.check_request do |_endpoint, data, _headers| - assert_match(/"threeDSecure\":{\"externalCardholderAuthenticationData\":{/, data) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/threeDSecure/, data) + assert_match(/externalCardholderAuthenticationData/, data) assert_match(/"eci\":\"05\"/, data) assert_match(/"cavv\":\"jJ81HADVRtXfCBATEp01CJUAAAA=\"/, data) assert_match(/"xid\":\"BwABBJQ1AgAAAAAgJDUCAAAAAAA=\"/, data) @@ -261,9 +416,9 @@ def test_successful_authorize_with_3ds_auth end def test_does_not_send_3ds_auth_when_empty - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, @options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_not_match(/threeDSecure/, data) assert_not_match(/externalCardholderAuthenticationData/, data) assert_not_match(/cavv/, data) @@ -281,9 +436,9 @@ def test_does_not_send_3ds_auth_when_empty def test_truncates_first_name_to_15_chars credit_card = credit_card('4567350000427977', { first_name: 'thisisaverylongfirstname' }) - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, credit_card, @options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/thisisaverylong/, data) end.respond_with(successful_authorize_response) @@ -294,15 +449,15 @@ def test_truncates_first_name_to_15_chars def test_handles_blank_names credit_card = credit_card('4567350000427977', { first_name: nil, last_name: nil }) - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, credit_card, @options) end.respond_with(successful_authorize_response) assert_success response end - def test_truncates_address_fields - response = stub_comms do + def test_truncates_split_address_fields + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, { billing_address: { address1: '1234 Supercalifragilisticexpialidociousthiscantbemorethanfiftycharacters', @@ -313,14 +468,15 @@ def test_truncates_address_fields country: 'US' } }) - end.check_request do |_endpoint, data, _headers| - refute_match(/Supercalifragilisticexpialidociousthiscantbemorethanfiftycharacters/, data) + end.check_request do |_method, _endpoint, data, _headers| + assert_equal(JSON.parse(data)['order']['customer']['billingAddress']['houseNumber'], '1234') + assert_equal(JSON.parse(data)['order']['customer']['billingAddress']['street'], 'Supercalifragilisticexpialidociousthiscantbemoreth') end.respond_with(successful_capture_response) assert_success response end def test_failed_authorize - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@rejected_amount, @declined_card, @options) end.respond_with(failed_authorize_response) @@ -329,21 +485,35 @@ def test_failed_authorize end def test_failed_capture - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.capture(100, '', @options) end.respond_with(failed_capture_response) assert_failure response end + def test_successful_inquire + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@accepted_amount, @credit_card, @options) + end.respond_with(successful_capture_response) + + assert_success response + + response = stub_comms(@gateway, :ssl_request) do + @gateway.inquire(response.authorization) + end.respond_with(successful_inquire_response) + + assert_success response + end + def test_successful_void - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, @options) end.respond_with(successful_capture_response) assert_success response - void = stub_comms do + void = stub_comms(@gateway, :ssl_request) do @gateway.void(response.authorization) end.respond_with(successful_void_response) @@ -351,9 +521,9 @@ def test_successful_void end def test_failed_void - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.void('5d53a33d960c46d00f5dc061947d998c') - end.check_request do |endpoint, _data, _headers| + end.check_request do |_method, endpoint, _data, _headers| assert_match(/5d53a33d960c46d00f5dc061947d998c/, endpoint) end.respond_with(failed_void_response) @@ -361,7 +531,7 @@ def test_failed_void end def test_successful_provider_unresponsive_void - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.void('5d53a33d960c46d00f5dc061947d998c') end.respond_with(successful_provider_unresponsive_void_response) @@ -369,7 +539,7 @@ def test_successful_provider_unresponsive_void end def test_failed_provider_unresponsive_void - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.void('5d53a33d960c46d00f5dc061947d998c') end.respond_with(failed_provider_unresponsive_void_response) @@ -377,7 +547,7 @@ def test_failed_provider_unresponsive_void end def test_successful_verify - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.verify(@credit_card, @options) end.respond_with(successful_authorize_response, successful_void_response) assert_equal '000000142800000000920000100001', response.authorization @@ -386,7 +556,7 @@ def test_successful_verify end def test_failed_verify - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.verify(@credit_card, @options) end.respond_with(failed_authorize_response) assert_equal '000000142800000000640000100001', response.authorization @@ -395,19 +565,19 @@ def test_failed_verify end def test_successful_refund - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, @options) end.respond_with(successful_authorize_response) assert_equal '000000142800000000920000100001', response.authorization - capture = stub_comms do + capture = stub_comms(@gateway, :ssl_request) do @gateway.capture(@accepted_amount, response.authorization) end.respond_with(successful_capture_response) - refund = stub_comms do + refund = stub_comms(@gateway, :ssl_request) do @gateway.refund(@accepted_amount, capture.authorization) - end.check_request do |endpoint, _data, _headers| + end.check_request do |_method, endpoint, _data, _headers| assert_match(/000000142800000000920000100001/, endpoint) end.respond_with(successful_refund_response) @@ -415,15 +585,15 @@ def test_successful_refund end def test_refund_passes_currency_code - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.refund(@accepted_amount, '000000142800000000920000100001', { currency: 'COP' }) - end.check_request do |_endpoint, data, _headers| + end.check_request do |_method, _endpoint, data, _headers| assert_match(/"currencyCode\":\"COP\"/, data) end.respond_with(failed_refund_response) end def test_failed_refund - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.refund(nil, '') end.respond_with(failed_refund_response) @@ -431,7 +601,7 @@ def test_failed_refund end def test_rejected_refund - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.refund(@accepted_amount, '000000142800000000920000100001') end.respond_with(rejected_refund_response) @@ -441,7 +611,7 @@ def test_rejected_refund end def test_invalid_raw_response - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, @options) end.respond_with(invalid_json_response) @@ -455,7 +625,7 @@ def test_scrub end def test_scrub_invalid_response - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, @options) end.respond_with(invalid_json_plus_card_data).message @@ -463,9 +633,9 @@ def test_scrub_invalid_response end def test_authorize_with_optional_idempotency_key_header - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, @options.merge(idempotency_key: 'test123')) - end.check_request do |_endpoint, _data, headers| + end.check_request do |_method, _endpoint, _data, headers| assert_equal headers['X-GCS-Idempotence-Key'], 'test123' end.respond_with(successful_authorize_response) @@ -622,6 +792,10 @@ def rejected_refund_response %({\n \"id\" : \"00000022184000047564000-100001\",\n \"refundOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 627000,\n \"currencyCode\" : \"COP\"\n },\n \"references\" : {\n \"merchantReference\" : \"17091GTgZmcC\",\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardRefundMethodSpecificOutput\" : {\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 1850,\n \"statusCodeChangeDateTime\" : \"20170313230631\"\n }\n}) end + def successful_inquire_response + %({\n \"payment\" : {\n \"id\" : \"000001263340000255950000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 126000,\n \"currencyCode\" : \"ARS\"\n },\n \"references\" : {\n \"merchantReference\" : \"10032994586\",\n \"paymentReference\" : \"0\",\n \"providerId\" : \"88\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"002792\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"493768******8095\",\n \"expiryDate\" : \"0824\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20220214193408\",\n \"isAuthorized\" : true\n }\n }\n }) + end + def successful_void_response %({\n \"payment\" : {\n \"id\" : \"000001263340000255950000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 126000,\n \"currencyCode\" : \"ARS\"\n },\n \"references\" : {\n \"merchantReference\" : \"10032994586\",\n \"paymentReference\" : \"0\",\n \"providerId\" : \"88\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"002792\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"493768******8095\",\n \"expiryDate\" : \"0824\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20220214193408\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n },\n \"cardPaymentMethodSpecificOutput\" : {\n \"voidResponseId\" : \"00\"\n }\n}) end diff --git a/test/unit/gateways/hps_test.rb b/test/unit/gateways/hps_test.rb index 6707d9d703e..1f8832e7ab7 100644 --- a/test/unit/gateways/hps_test.rb +++ b/test/unit/gateways/hps_test.rb @@ -288,11 +288,13 @@ def test_account_number_scrubbing def test_successful_purchase_with_apple_pay_raw_cryptogram_with_eci @gateway.expects(:ssl_post).returns(successful_charge_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -301,11 +303,13 @@ def test_successful_purchase_with_apple_pay_raw_cryptogram_with_eci def test_failed_purchase_with_apple_pay_raw_cryptogram_with_eci @gateway.expects(:ssl_post).returns(failed_charge_response_decline) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_failure response assert_equal 'The card was declined.', response.message @@ -314,10 +318,12 @@ def test_failed_purchase_with_apple_pay_raw_cryptogram_with_eci def test_successful_purchase_with_apple_pay_raw_cryptogram_without_eci @gateway.expects(:ssl_post).returns(successful_charge_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -326,10 +332,12 @@ def test_successful_purchase_with_apple_pay_raw_cryptogram_without_eci def test_failed_purchase_with_apple_pay_raw_cryptogram_without_eci @gateway.expects(:ssl_post).returns(failed_charge_response_decline) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_failure response assert_equal 'The card was declined.', response.message @@ -338,11 +346,13 @@ def test_failed_purchase_with_apple_pay_raw_cryptogram_without_eci def test_successful_auth_with_apple_pay_raw_cryptogram_with_eci @gateway.expects(:ssl_post).returns(successful_authorize_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -351,11 +361,13 @@ def test_successful_auth_with_apple_pay_raw_cryptogram_with_eci def test_failed_auth_with_apple_pay_raw_cryptogram_with_eci @gateway.expects(:ssl_post).returns(failed_authorize_response_decline) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_failure response assert_equal 'The card was declined.', response.message @@ -364,10 +376,12 @@ def test_failed_auth_with_apple_pay_raw_cryptogram_with_eci def test_successful_auth_with_apple_pay_raw_cryptogram_without_eci @gateway.expects(:ssl_post).returns(successful_authorize_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -376,10 +390,12 @@ def test_successful_auth_with_apple_pay_raw_cryptogram_without_eci def test_failed_auth_with_apple_pay_raw_cryptogram_without_eci @gateway.expects(:ssl_post).returns(failed_authorize_response_decline) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :apple_pay) + source: :apple_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_failure response assert_equal 'The card was declined.', response.message @@ -388,11 +404,13 @@ def test_failed_auth_with_apple_pay_raw_cryptogram_without_eci def test_successful_purchase_with_android_pay_raw_cryptogram_with_eci @gateway.expects(:ssl_post).returns(successful_charge_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :android_pay) + source: :android_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -401,11 +419,13 @@ def test_successful_purchase_with_android_pay_raw_cryptogram_with_eci def test_failed_purchase_with_android_pay_raw_cryptogram_with_eci @gateway.expects(:ssl_post).returns(failed_charge_response_decline) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :android_pay) + source: :android_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_failure response assert_equal 'The card was declined.', response.message @@ -414,10 +434,12 @@ def test_failed_purchase_with_android_pay_raw_cryptogram_with_eci def test_successful_purchase_with_android_pay_raw_cryptogram_without_eci @gateway.expects(:ssl_post).returns(successful_charge_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :android_pay) + source: :android_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -426,10 +448,12 @@ def test_successful_purchase_with_android_pay_raw_cryptogram_without_eci def test_failed_purchase_with_android_pay_raw_cryptogram_without_eci @gateway.expects(:ssl_post).returns(failed_charge_response_decline) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :android_pay) + source: :android_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_failure response assert_equal 'The card was declined.', response.message @@ -438,11 +462,13 @@ def test_failed_purchase_with_android_pay_raw_cryptogram_without_eci def test_successful_auth_with_android_pay_raw_cryptogram_with_eci @gateway.expects(:ssl_post).returns(successful_authorize_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :android_pay) + source: :android_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -451,11 +477,13 @@ def test_successful_auth_with_android_pay_raw_cryptogram_with_eci def test_failed_auth_with_android_pay_raw_cryptogram_with_eci @gateway.expects(:ssl_post).returns(failed_authorize_response_decline) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :android_pay) + source: :android_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_failure response assert_equal 'The card was declined.', response.message @@ -464,10 +492,12 @@ def test_failed_auth_with_android_pay_raw_cryptogram_with_eci def test_successful_auth_with_android_pay_raw_cryptogram_without_eci @gateway.expects(:ssl_post).returns(successful_authorize_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :android_pay) + source: :android_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -476,10 +506,12 @@ def test_successful_auth_with_android_pay_raw_cryptogram_without_eci def test_failed_auth_with_android_pay_raw_cryptogram_without_eci @gateway.expects(:ssl_post).returns(failed_authorize_response_decline) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :android_pay) + source: :android_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_failure response assert_equal 'The card was declined.', response.message @@ -488,11 +520,13 @@ def test_failed_auth_with_android_pay_raw_cryptogram_without_eci def test_successful_purchase_with_google_pay_raw_cryptogram_with_eci @gateway.expects(:ssl_post).returns(successful_charge_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :google_pay) + source: :google_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -501,11 +535,13 @@ def test_successful_purchase_with_google_pay_raw_cryptogram_with_eci def test_failed_purchase_with_google_pay_raw_cryptogram_with_eci @gateway.expects(:ssl_post).returns(failed_charge_response_decline) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :google_pay) + source: :google_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_failure response assert_equal 'The card was declined.', response.message @@ -514,10 +550,12 @@ def test_failed_purchase_with_google_pay_raw_cryptogram_with_eci def test_successful_purchase_with_google_pay_raw_cryptogram_without_eci @gateway.expects(:ssl_post).returns(successful_charge_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :google_pay) + source: :google_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -526,10 +564,12 @@ def test_successful_purchase_with_google_pay_raw_cryptogram_without_eci def test_failed_purchase_with_google_pay_raw_cryptogram_without_eci @gateway.expects(:ssl_post).returns(failed_charge_response_decline) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :google_pay) + source: :google_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_failure response assert_equal 'The card was declined.', response.message @@ -538,11 +578,13 @@ def test_failed_purchase_with_google_pay_raw_cryptogram_without_eci def test_successful_auth_with_google_pay_raw_cryptogram_with_eci @gateway.expects(:ssl_post).returns(successful_authorize_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :google_pay) + source: :google_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -551,11 +593,13 @@ def test_successful_auth_with_google_pay_raw_cryptogram_with_eci def test_failed_auth_with_google_pay_raw_cryptogram_with_eci @gateway.expects(:ssl_post).returns(failed_authorize_response_decline) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, eci: '05', - source: :google_pay) + source: :google_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_failure response assert_equal 'The card was declined.', response.message @@ -564,10 +608,12 @@ def test_failed_auth_with_google_pay_raw_cryptogram_with_eci def test_successful_auth_with_google_pay_raw_cryptogram_without_eci @gateway.expects(:ssl_post).returns(successful_authorize_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :google_pay) + source: :google_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response assert_equal 'Success', response.message @@ -576,10 +622,12 @@ def test_successful_auth_with_google_pay_raw_cryptogram_without_eci def test_failed_auth_with_google_pay_raw_cryptogram_without_eci @gateway.expects(:ssl_post).returns(failed_authorize_response_decline) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', verification_value: nil, - source: :google_pay) + source: :google_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_failure response assert_equal 'The card was declined.', response.message diff --git a/test/unit/gateways/ipg_test.rb b/test/unit/gateways/ipg_test.rb index e49b2c56d6a..255fbea4dce 100644 --- a/test/unit/gateways/ipg_test.rb +++ b/test/unit/gateways/ipg_test.rb @@ -118,6 +118,17 @@ def test_successful_purchase_with_recurring_type assert_success response end + def test_successful_purchase_with_store_id + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ store_id: '1234' })) + end.check_request do |_endpoint, data, _headers| + doc = REXML::Document.new(data) + assert_match('1234', REXML::XPath.first(doc, '//v1:StoreId').text) + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_successful_purchase_with_payment_token payment_token = 'ABC123' @@ -162,7 +173,7 @@ def test_failed_purchase response = @gateway.purchase(@amount, @credit_card, @options) assert_failure response - assert_equal 'DECLINED', response.message + assert_match 'DECLINED', response.message end def test_successful_authorize @@ -183,7 +194,7 @@ def test_failed_authorize response = @gateway.authorize(@amount, @credit_card, @options.merge!({ order_id: 'ORD03' })) assert_failure response - assert_equal 'FAILED', response.message + assert_match 'FAILED', response.message end def test_successful_capture @@ -204,7 +215,7 @@ def test_failed_capture response = @gateway.capture(@amount, '123', @options) assert_failure response - assert_equal 'FAILED', response.message + assert_match 'FAILED', response.message end def test_successful_refund @@ -225,7 +236,7 @@ def test_failed_refund response = @gateway.refund(@amount, '123', @options) assert_failure response - assert_equal 'FAILED', response.message + assert_match 'FAILED', response.message end def test_successful_void @@ -246,7 +257,7 @@ def test_failed_void response = @gateway.void('', @options) assert_failure response - assert_equal 'FAILED', response.message + assert_match 'FAILED', response.message end def test_successful_verify @@ -321,6 +332,48 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_store_and_user_id_from_with_complete_credentials + test_combined_user_id = 'WS5921102002._.1' + split_credentials = @gateway.send(:store_and_user_id_from, test_combined_user_id) + + assert_equal '5921102002', split_credentials[:store_id] + assert_equal '1', split_credentials[:user_id] + end + + def test_store_and_user_id_from_missing_store_id_prefix + test_combined_user_id = '5921102002._.1' + split_credentials = @gateway.send(:store_and_user_id_from, test_combined_user_id) + + assert_equal '5921102002', split_credentials[:store_id] + assert_equal '1', split_credentials[:user_id] + end + + def test_store_and_user_id_misplaced_store_id_prefix + test_combined_user_id = '5921102002WS._.1' + split_credentials = @gateway.send(:store_and_user_id_from, test_combined_user_id) + + assert_equal '5921102002WS', split_credentials[:store_id] + assert_equal '1', split_credentials[:user_id] + end + + def test_store_and_user_id_from_missing_delimiter + test_combined_user_id = 'WS59211020021' + split_credentials = @gateway.send(:store_and_user_id_from, test_combined_user_id) + + assert_equal '59211020021', split_credentials[:store_id] + assert_equal nil, split_credentials[:user_id] + end + + def test_message_from_just_with_transaction_result + am_response = { TransactionResult: 'success !' } + assert_equal 'success !', @gateway.send(:message_from, am_response) + end + + def test_message_from_with_an_error + am_response = { TransactionResult: 'DECLINED', ErrorMessage: 'CODE: this is an error message' } + assert_equal 'DECLINED, this is an error message', @gateway.send(:message_from, am_response) + end + private def successful_purchase_response diff --git a/test/unit/gateways/iveri_test.rb b/test/unit/gateways/iveri_test.rb index 329514fafa5..c193f6c05d7 100644 --- a/test/unit/gateways/iveri_test.rb +++ b/test/unit/gateways/iveri_test.rb @@ -25,6 +25,17 @@ def test_successful_purchase assert response.test? end + def test_successful_purchase_with_iveri_url + @gateway = IveriGateway.new(app_id: '123', cert_id: '321', url_override: 'iveri') + + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal '{F0568958-D10B-4093-A3BF-663168B06140}|{5CEF96FD-960E-4EA5-811F-D02CE6E36A96}|48b63446223ce91451fc3c1641a9ec03', response.authorization + assert response.test? + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) diff --git a/test/unit/gateways/kushki_test.rb b/test/unit/gateways/kushki_test.rb index 8a7ec9acd45..f623b6b4ec9 100644 --- a/test/unit/gateways/kushki_test.rb +++ b/test/unit/gateways/kushki_test.rb @@ -41,7 +41,11 @@ def test_successful_purchase_with_options metadata: { productos: 'bananas', nombre_apellido: 'Kirk' - } + }, + months: 2, + deferred_grace_months: '05', + deferred_credit_type: '01', + deferred_months: 3 } amount = 100 * ( @@ -58,6 +62,10 @@ def test_successful_purchase_with_options @gateway.purchase(amount, @credit_card, options) end.check_request do |_endpoint, data, _headers| assert_includes data, 'metadata' + assert_includes data, 'months' + assert_includes data, 'deferred_grace_month' + assert_includes data, 'deferred_credit_type' + assert_includes data, 'deferred_months' end.respond_with(successful_token_response, successful_charge_response) assert_success response @@ -128,7 +136,7 @@ def test_partial_taxes_do_not_error assert_success response end - def test_taxes_are_included_when_provided + def test_cop_taxes_are_included_when_provided options = { currency: 'COP', amount: { @@ -164,6 +172,42 @@ def test_taxes_are_included_when_provided assert_success response end + def test_usd_taxes_are_included_when_provided + options = { + currency: 'USD', + amount: { + subtotal_iva_0: '4.95', + subtotal_iva: '10', + iva: '1.54', + ice: '3.50', + extra_taxes: { + propina: 0.1, + tasa_aeroportuaria: 0.2, + agencia_de_viaje: 0.3, + iac: 0.4 + } + } + } + + amount = 100 * ( + options[:amount][:subtotal_iva_0].to_f + + options[:amount][:subtotal_iva].to_f + + options[:amount][:iva].to_f + + options[:amount][:ice].to_f + ) + + response = stub_comms do + @gateway.purchase(amount, @credit_card, options) + end.check_request do |endpoint, data, _headers| + if /charges/.match?(endpoint) + assert_match %r{extraTaxes}, data + assert_match %r{propina}, data + end + end.respond_with(successful_charge_response, successful_token_response) + + assert_success response + end + def test_failed_purchase options = { amount: { diff --git a/test/unit/gateways/litle_test.rb b/test/unit/gateways/litle_test.rb index 3531e9e3eb3..4c2314894aa 100644 --- a/test/unit/gateways/litle_test.rb +++ b/test/unit/gateways/litle_test.rb @@ -76,7 +76,35 @@ def test_successful_purchase end.respond_with(successful_purchase_response) assert_success response + assert_equal 'Approved', response.message + assert_equal '100000000000000006;sale;100', response.authorization + assert response.test? + end + def test_successful_purchase_with_010_response + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.check_request do |endpoint, _data, _headers| + # Counterpoint to test_successful_postlive_url: + assert_match(/www\.testvantivcnp\.com/, endpoint) + end.respond_with(successful_purchase_response('010', 'Partially Approved')) + + assert_success response + assert_equal 'Partially Approved: The authorized amount is less than the requested amount.', response.message + assert_equal '100000000000000006;sale;100', response.authorization + assert response.test? + end + + def test_successful_purchase_with_001_response + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.check_request do |endpoint, _data, _headers| + # Counterpoint to test_successful_postlive_url: + assert_match(/www\.testvantivcnp\.com/, endpoint) + end.respond_with(successful_purchase_response('001', 'Transaction Received')) + + assert_success response + assert_equal 'Transaction Received: This is sent to acknowledge that the submitted transaction has been received.', response.message assert_equal '100000000000000006;sale;100', response.authorization assert response.test? end @@ -112,6 +140,22 @@ def test_successful_purchase_with_echeck assert response.test? end + def test_sale_response_duplicate_attribute + dup_response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(duplicate_purchase_response) + + assert_success dup_response + assert_true dup_response.params['duplicate'] + + non_dup_response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_purchase_response) + + assert_success non_dup_response + assert_false non_dup_response.params['duplicate'] + end + def test_failed_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card) @@ -221,6 +265,14 @@ def test_passing_debt_repayment end.respond_with(successful_authorize_response) end + def test_fraud_filter_override + stub_comms do + @gateway.authorize(@amount, @credit_card, { fraud_filter_override: true }) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(true), data) + end.respond_with(successful_authorize_response) + end + def test_passing_payment_cryptogram stub_comms do @gateway.purchase(@amount, @decrypted_apple_pay) @@ -708,10 +760,29 @@ def network_transaction_id '63225578415568556365452427825' end - def successful_purchase_response + def successful_purchase_response(code = '000', message = 'Approved') %( + 100000000000000006 + 1 + #{code} + 2014-03-31T11:34:39 + #{message} + 11111 + + 01 + M + + + + ) + end + + def duplicate_purchase_response + %( + + 100000000000000006 1 000 diff --git a/test/unit/gateways/mercado_pago_test.rb b/test/unit/gateways/mercado_pago_test.rb index 679e7af2979..a25401c202d 100644 --- a/test/unit/gateways/mercado_pago_test.rb +++ b/test/unit/gateways/mercado_pago_test.rb @@ -6,24 +6,30 @@ class MercadoPagoTest < Test::Unit::TestCase def setup @gateway = MercadoPagoGateway.new(access_token: 'access_token') @credit_card = credit_card - @elo_credit_card = credit_card('5067268650517446', + @elo_credit_card = credit_card( + '5067268650517446', month: 10, year: 2020, first_name: 'John', last_name: 'Smith', - verification_value: '737') - @cabal_credit_card = credit_card('6035227716427021', + verification_value: '737' + ) + @cabal_credit_card = credit_card( + '6035227716427021', month: 10, year: 2020, first_name: 'John', last_name: 'Smith', - verification_value: '737') - @naranja_credit_card = credit_card('5895627823453005', + verification_value: '737' + ) + @naranja_credit_card = credit_card( + '5895627823453005', month: 10, year: 2020, first_name: 'John', last_name: 'Smith', - verification_value: '123') + verification_value: '123' + ) @amount = 100 @options = { @@ -283,6 +289,29 @@ def test_failed_verify assert response.test? end + def test_successful_inquire_with_id + @gateway.expects(:ssl_get).returns(successful_authorize_response) + + response = @gateway.inquire('authorization|amount') + assert_success response + + assert_equal '4261941|', response.authorization + assert_equal 'pending_capture', response.message + assert response.test? + end + + def test_successful_inquire_with_external_reference + @gateway.expects(:ssl_get).returns(successful_search_payments_response) + + response = @gateway.inquire(nil, { external_reference: '1234' }) + assert_success response + + assert_equal '1234', response.params['external_reference'] + assert_equal '1|', response.authorization + assert_equal 'accredited', response.message + assert response.test? + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed @@ -707,4 +736,10 @@ def successful_purchase_with_metadata_response {"id":4141491,"date_created":"2017-07-06T09:49:35.000-04:00","date_approved":"2017-07-06T09:49:35.000-04:00","date_last_updated":"2017-07-06T09:49:35.000-04:00","date_of_expiration":null,"money_release_date":"2017-07-18T09:49:35.000-04:00","operation_type":"regular_payment","issuer_id":"166","payment_method_id":"visa","payment_type_id":"credit_card","status":"approved","status_detail":"accredited","currency_id":"MXN","description":"Store Purchase","live_mode":false,"sponsor_id":null,"authorization_code":null,"related_exchange_rate":null,"collector_id":261735089,"payer":{"type":"guest","id":null,"email":"user@example.com","identification":{"type":null,"number":null},"phone":{"area_code":null,"number":null,"extension":""},"first_name":"First User","last_name":"User","entity_type":null},"metadata":{"key_1":"value_1","key_2":"value_2","key_3":{"nested_key_1":"value_3"}},"additional_info":{"payer":{"address":{"zip_code":"K1C2N6","street_name":"My Street","street_number":"456"}}},"order":{"type":"mercadopago","id":"2326513804447055222"},"external_reference":null,"transaction_amount":5,"transaction_amount_refunded":0,"coupon_amount":0,"differential_pricing_id":null,"deduction_schema":null,"transaction_details":{"net_received_amount":0.14,"total_paid_amount":5,"overpaid_amount":0,"external_resource_url":null,"installment_amount":5,"financial_institution":null,"payment_method_reference_id":null,"payable_deferral_period":null,"acquirer_reference":null},"fee_details":[{"type":"mercadopago_fee","amount":4.86,"fee_payer":"collector"}],"captured":true,"binary_mode":false,"call_for_authorize_id":null,"statement_descriptor":"WWW.MERCADOPAGO.COM","installments":1,"card":{"id":null,"first_six_digits":"450995","last_four_digits":"3704","expiration_month":9,"expiration_year":2018,"date_created":"2017-07-06T09:49:35.000-04:00","date_last_updated":"2017-07-06T09:49:35.000-04:00","cardholder":{"name":"Longbob Longsen","identification":{"number":null,"type":null}}},"notification_url":null,"refunds":[],"processing_mode":null,"merchant_account_id":null,"acquirer":null,"merchant_number":null} ) end + + def successful_search_payments_response + %( + [{"paging":{"total":17493,"limit":30,"offset":0},"results":[{"id":1,"date_created":"2017-08-31T11:26:38.000Z","date_approved":"2017-08-31T11:26:38.000Z","date_last_updated":"2017-08-31T11:26:38.000Z","money_release_date":"2017-09-14T11:26:38.000Z","payment_method_id":"account_money","payment_type_id":"credit_card","status":"approved","status_detail":"accredited","currency_id":"BRL","description":"PagoPizza","collector_id":2,"payer":{"id":123,"email":"afriend@gmail.com","identification":{"type":"DNI","number":12345678},"type":"customer"},"metadata":{},"additional_info":{},"external_reference":"1234","transaction_amount":250,"transaction_amount_refunded":0,"coupon_amount":0,"transaction_details":{"net_received_amount":250,"total_paid_amount":250,"overpaid_amount":0,"installment_amount":250},"installments":1,"card":{}}]}] + ) + end end diff --git a/test/unit/gateways/merchant_e_solutions_test.rb b/test/unit/gateways/merchant_e_solutions_test.rb index 7cced6c3594..228ac4e1305 100644 --- a/test/unit/gateways/merchant_e_solutions_test.rb +++ b/test/unit/gateways/merchant_e_solutions_test.rb @@ -19,6 +19,16 @@ def setup billing_address: address, description: 'Store Purchase' } + + @stored_credential_options = { + moto_ecommerce_ind: '7', + client_reference_number: '345892', + recurring_pmt_num: 11, + recurring_pmt_count: 10, + card_on_file: 'Y', + cit_mit_indicator: 'C101', + account_data_source: 'Y' + } end def test_successful_purchase @@ -30,6 +40,20 @@ def test_successful_purchase assert response.test? end + def test_successful_purchase_with_stored_credentials + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @stored_credential_options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/moto_ecommerce_ind=7/, data) + assert_match(/client_reference_number=345892/, data) + assert_match(/recurring_pmt_num=11/, data) + assert_match(/recurring_pmt_count=10/, data) + assert_match(/card_on_file=Y/, data) + assert_match(/cit_mit_indicator=C101/, data) + assert_match(/account_data_source=Y/, data) + end.respond_with(successful_purchase_response) + end + def test_unsuccessful_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -84,14 +108,6 @@ def test_void assert response.test? end - def test_store - @gateway.expects(:ssl_post).returns(successful_store_response) - assert response = @gateway.store(@credit_card) - assert response.success? - assert_equal 'ae641b57b19b3bb89faab44191479872', response.authorization - assert response.test? - end - def test_unstore @gateway.expects(:ssl_post).returns(successful_unstore_response) assert response = @gateway.unstore('ae641b57b19b3bb89faab44191479872') @@ -100,6 +116,18 @@ def test_unstore assert response.test? end + def test_successful_verify + response = stub_comms(@gateway, :ssl_request) do + @gateway.verify(@credit_card, { store_card: 'y' }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/transaction_type=A/, data) + assert_match(/store_card=y/, data) + assert_match(/card_number=#{@credit_card.number}/, data) + end.respond_with(successful_verify_response) + assert_success response + assert_equal 'Card Ok', response.message + end + def test_successful_avs_check @gateway.expects(:ssl_post).returns(successful_purchase_response + '&avs_result=Y') assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -202,6 +230,10 @@ def successful_unstore_response 'transaction_id=d79410c91b4b31ba99f5a90558565df9&error_code=000&auth_response_text=Stored Card Data Deleted' end + def successful_verify_response + 'transaction_id=a5ef059bff7a3f75ac2398eea4cc73cd&error_code=085&auth_response_text=Card Ok&avs_result=0&cvv2_result=M&auth_code=T1933H' + end + def failed_purchase_response 'transaction_id=error&error_code=101&auth_response_text=Invalid%20I%20or%20Key%20Incomplete%20Request' end diff --git a/test/unit/gateways/mit_test.rb b/test/unit/gateways/mit_test.rb index 16a80355f4f..4ae7b4932be 100644 --- a/test/unit/gateways/mit_test.rb +++ b/test/unit/gateways/mit_test.rb @@ -169,88 +169,100 @@ def failed_void_response end def pre_scrubbed - <<-PRE_SCRUBBED - starting SSL for wpy.mitec.com.mx:443... - SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 - <- "POST /ModuloUtilWS/activeCDP.htm HTTP/1.1\r\nContent-Type: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: wpy.mitec.com.mx\r\nContent-Length: 607\r\n\r\n" - <- "{\"payload\":\"1aUSihtRXgd+1nycRfVWgv0JDZsGLsrpsNkahpkx4jmnBRRAPPao+zJYqsN4xrGMIeVdJ3Y5LlQYXg5qu8O7iZmDPTqWbyKmsurCxJidr6AkFszwvRfugElyb5sAYpUcrnFSpVUgz2NGcIuMRalr0irf7q30+TzbLRHQc1Z5QTe6am3ndO8aSKKLwYYmfHcO8E/+dPiCsSP09P2heNqpMbf5IKdSwGCVS1Rtpcoijl3wXB8zgeBZ1PXHAmmkC1/CWRs/fh1qmvYFzb8YAiRy5q80Tyq09IaeSpQ1ydq3r95QBSJy6H4gz2OV/v2xdm1A63XEh2+6N6p2XDyzGWQrxKE41wmqRCxie7qY2xqdv4S8Cl8ldSMEpZY46A68hKIN6zrj6eMWxauwdi6ZkZfMDuh9Pn9x5gwwgfElLopIpR8fejB6G4hAQHtq2jhn5D4ccmAqNxkrB4w5k+zc53Rupk2u3MDp5T5sRkqvNyIN2kCE6i0DD9HlqkCjWV+bG9WcUiO4D7m5fWRE5f9OQ2XjeA==IVCA33721\"}" - -> "HTTP/1.1 200 \r\n" - -> "Strict-Transport-Security: max-age=31536000;includeSubDomains\r\n" - -> "X-Content-Type-Options: nosniff\r\n" - -> "X-XSS-Protection: 1; mode=block\r\n" - -> "Content-Type: text/html;charset=ISO-8859-1\r\n" - -> "Content-Length: 320\r\n" - -> "Date: Mon, 06 Sep 2021 19:02:08 GMT\r\n" - -> "Connection: close\r\n" - -> "Server: \r\n" - -> "Set-Cookie: UqZBpD3n=v1I4cyJQ__N2M; Expires=Mon, 06-Sep-2021 19:03:38 GMT; Path=/; Secure; HttpOnly\r\n" - -> "\r\n" - reading 320 bytes... - -> "hl0spHqAAamtY47Vo+W+dZcpDyK8QRqpx/gWzIM1F3X1VFV/zNUcKCuqaSL6F4S7MqOGUMOC3BXIZYaS9TpJf6xsMYeRDyMpiv+sE0VpY2a4gULhLv1ztgGHgF3OpMjD8ucgLbd9FMA5OZjd8wlaqn46JCiYNcNIPV7hkHWNCqSWow+C+SSkWZeaa9YpNT3E6udixbog30/li1FcSI+Ti80EWBIdH3JDcQvjQbqecNb87JYad0EhgqL1o7ZEMehfZ2kW9FG6OXjGzWyhiWd2GEFKe8em4vEJxARFdXsaHe3tX0jqnF2gYOiFRclqFkbk" - read 320 bytes - Conn close - opening connection to wpy.mitec.com.mx:443... - opened - starting SSL for wpy.mitec.com.mx:443... - SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 - <- "POST /ModuloUtilWS/activeCDP.htm HTTP/1.1\r\nContent-Type: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: wpy.mitec.com.mx\r\nContent-Length: 359\r\n\r\n" - <- "{\"payload\":\"Z6l24tZG2YfTOQTne8NVygr/YeuVRNya8ZUCM5NvRgOEL/Mt8PO0voNnspoiFSg+RVamC4V2BipmU3spPVBg6Dr0xMpPL7ryVB9mlM4PokUdHkZTjXJHbbr1GWdyEPMYYSH0f+M1qUDO57EyUuZv8o6QSv+a/tuOrrBwsHI8cnsv+y9qt5L9LuGRMeBYvZkkK+xw53eDqYsJGoCvpk/pljCCkGU7Q/sKsLOx0MT6dA/BLVGrGeo8ngO+W/cnOigGfIZJSPFTcrUKI/Q7AsHuP+3lG6q9VAri9UJZXm5pWOg=IVCA33721\"}" - -> "HTTP/1.1 200 \r\n" - -> "Strict-Transport-Security: max-age=31536000;includeSubDomains\r\n" - -> "X-Content-Type-Options: nosniff\r\n" - -> "X-XSS-Protection: 1; mode=block\r\n" - -> "Content-Type: text/html;charset=ISO-8859-1\r\n" - -> "Content-Length: 280\r\n" - -> "Date: Mon, 06 Sep 2021 19:02:08 GMT\r\n" - -> "Connection: close\r\n" - -> "Server: \r\n" - -> "Set-Cookie: UqZBpD3n=v1JocyJQ__9tu; Expires=Mon, 06-Sep-2021 19:03:39 GMT; Path=/; Secure; HttpOnly\r\n" - -> "\r\n" - reading 280 bytes... - -> "BnuAgMOx9USBreICk027VY2ZqJA7xQcRT9Ytz8WpabDnqIglj43J/I03pKLtDlFrerKIAzhW1YCroDOS7mvtA5YnWezLstoOK0LbIcYqLzj1dCFW2zLb9ssTCxJa6ZmEQdzQdl8pyY4mC0QQ0JrOrsSA9QfX1XhkdcSVnsxQV1cEooL8/6EsVFCb6yVIMhVnGL6GRCc2J+rPigHsljLWRovgRKqFIURJjNWbfqepDRPG2hCNKsabM/lE2DFtKLMs4J5iwY9HiRbrAMG6BaGNiQ==" - read 280 bytes - Conn close + <<~PRE_SCRUBBED + starting SSL for wpy.mitec.com.mx:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 + <- "POST /ModuloUtilWS/activeCDP.htm HTTP/1.1\r\nContent-Type: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: wpy.mitec.com.mx\r\nContent-Length: 607\r\n\r\n" + <- "{\"payload\":\"1aUSihtRXgd+1nycRfVWgv0JDZsGLsrpsNkahpkx4jmnBRRAPPao+zJYqsN4xrGMIeVdJ3Y5LlQYXg5qu8O7iZmDPTqWbyKmsurCxJidr6AkFszwvRfugElyb5sAYpUcrnFSpVUgz2NGcIuMRalr0irf7q30+TzbLRHQc1Z5QTe6am3ndO8aSKKLwYYmfHcO8E/+dPiCsSP09P2heNqpMbf5IKdSwGCVS1Rtpcoijl3wXB8zgeBZ1PXHAmmkC1/CWRs/fh1qmvYFzb8YAiRy5q80Tyq09IaeSpQ1ydq3r95QBSJy6H4gz2OV/v2xdm1A63XEh2+6N6p2XDyzGWQrxKE41wmqRCxie7qY2xqdv4S8Cl8ldSMEpZY46A68hKIN6zrj6eMWxauwdi6ZkZfMDuh9Pn9x5gwwgfElLopIpR8fejB6G4hAQHtq2jhn5D4ccmAqNxkrB4w5k+zc53Rupk2u3MDp5T5sRkqvNyIN2kCE6i0DD9HlqkCjWV+bG9WcUiO4D7m5fWRE5f9OQ2XjeA==IVCA33721\"}" + -> "HTTP/1.1 200 \r\n" + -> "Strict-Transport-Security: max-age=31536000;includeSubDomains\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "Content-Type: text/html;charset=ISO-8859-1\r\n" + -> "Content-Length: 320\r\n" + -> "Date: Mon, 06 Sep 2021 19:02:08 GMT\r\n" + -> "Connection: close\r\n" + -> "Server: \r\n" + -> "Set-Cookie: UqZBpD3n=v1I4cyJQ__N2M; Expires=Mon, 06-Sep-2021 19:03:38 GMT; Path=/; Secure; HttpOnly\r\n" + -> "\r\n" + reading 320 bytes... + -> "hl0spHqAAamtY47Vo+W+dZcpDyK8QRqpx/gWzIM1F3X1VFV/zNUcKCuqaSL6F4S7MqOGUMOC3BXIZYaS9TpJf6xsMYeRDyMpiv+sE0VpY2a4gULhLv1ztgGHgF3OpMjD8ucgLbd9FMA5OZjd8wlaqn46JCiYNcNIPV7hkHWNCqSWow+C+SSkWZeaa9YpNT3E6udixbog30/li1FcSI+Ti80EWBIdH3JDcQvjQbqecNb87JYad0EhgqL1o7ZEMehfZ2kW9FG6OXjGzWyhiWd2GEFKe8em4vEJxARFdXsaHe3tX0jqnF2gYOiFRclqFkbk" + read 320 bytes + Conn close + opening connection to wpy.mitec.com.mx:443... + opened + starting SSL for wpy.mitec.com.mx:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 + <- "POST /ModuloUtilWS/activeCDP.htm HTTP/1.1\r\nContent-Type: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: wpy.mitec.com.mx\r\nContent-Length: 359\r\n\r\n" + <- "{\"payload\":\"Z6l24tZG2YfTOQTne8NVygr/YeuVRNya8ZUCM5NvRgOEL/Mt8PO0voNnspoiFSg+RVamC4V2BipmU3spPVBg6Dr0xMpPL7ryVB9mlM4PokUdHkZTjXJHbbr1GWdyEPMYYSH0f+M1qUDO57EyUuZv8o6QSv+a/tuOrrBwsHI8cnsv+y9qt5L9LuGRMeBYvZkkK+xw53eDqYsJGoCvpk/pljCCkGU7Q/sKsLOx0MT6dA/BLVGrGeo8ngO+W/cnOigGfIZJSPFTcrUKI/Q7AsHuP+3lG6q9VAri9UJZXm5pWOg=IVCA33721\"}" + -> "HTTP/1.1 200 \r\n" + -> "Strict-Transport-Security: max-age=31536000;includeSubDomains\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "Content-Type: text/html;charset=ISO-8859-1\r\n" + -> "Content-Length: 280\r\n" + -> "Date: Mon, 06 Sep 2021 19:02:08 GMT\r\n" + -> "Connection: close\r\n" + -> "Server: \r\n" + -> "Set-Cookie: UqZBpD3n=v1JocyJQ__9tu; Expires=Mon, 06-Sep-2021 19:03:39 GMT; Path=/; Secure; HttpOnly\r\n" + -> "\r\n" + reading 280 bytes... + -> "BnuAgMOx9USBreICk027VY2ZqJA7xQcRT9Ytz8WpabDnqIglj43J/I03pKLtDlFrerKIAzhW1YCroDOS7mvtA5YnWezLstoOK0LbIcYqLzj1dCFW2zLb9ssTCxJa6ZmEQdzQdl8pyY4mC0QQ0JrOrsSA9QfX1XhkdcSVnsxQV1cEooL8/6EsVFCb6yVIMhVnGL6GRCc2J+rPigHsljLWRovgRKqFIURJjNWbfqepDRPG2hCNKsabM/lE2DFtKLMs4J5iwY9HiRbrAMG6BaGNiQ==" + read 280 bytes + Conn close PRE_SCRUBBED end def post_scrubbed - <<-POST_SCRUBBED - starting SSL for wpy.mitec.com.mx:443... - SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 - <- "POST /ModuloUtilWS/activeCDP.htm HTTP/1.1\r\nContent-Type: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: wpy.mitec.com.mx\r\nContent-Length: 607\r\n\r\n" - <- "{\"payload\":\"{"operation":"Authorize","commerce_id":"147","user":"IVCA33721","apikey":"[FILTERED]","testMode":"YES","amount":"11.15","currency":"MXN","reference":"721","transaction_id":"721","installments":1,"card":"[FILTERED]","expmonth":9,"expyear":2025,"cvv":"[FILTERED]","name_client":"Pedro Flores Valdes","email":"nadie@mit.test","key_session":"[FILTERED]"}IVCA33721\"}" - -> "HTTP/1.1 200 \r\n" - -> "Strict-Transport-Security: max-age=31536000;includeSubDomains\r\n" - -> "X-Content-Type-Options: nosniff\r\n" - -> "X-XSS-Protection: 1; mode=block\r\n" - -> "Content-Type: text/html;charset=ISO-8859-1\r\n" - -> "Content-Length: 320\r\n" - -> "Date: Mon, 06 Sep 2021 19:02:08 GMT\r\n" - -> "Connection: close\r\n" - -> "Server: \r\n" - -> "Set-Cookie: UqZBpD3n=v1I4cyJQ__N2M; Expires=Mon, 06-Sep-2021 19:03:38 GMT; Path=/; Secure; HttpOnly\r\n" - -> "\r\n" - response: {"folio_cdp":"095492846","auth":"928468","response":"approved","message":"0C- Pago aprobado (test)","id_comercio":"147","reference":"721","amount":"11.15","time":"19:02:08 06:09:2021","operation":"Authorize"}read 320 bytes - Conn close - opening connection to wpy.mitec.com.mx:443... - opened - starting SSL for wpy.mitec.com.mx:443... - SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 - <- "POST /ModuloUtilWS/activeCDP.htm HTTP/1.1\r\nContent-Type: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: wpy.mitec.com.mx\r\nContent-Length: 359\r\n\r\n" - <- "{\"payload\":\"{"operation":"Capture","commerce_id":"147","user":"IVCA33721","apikey":"[FILTERED]","testMode":"YES","transaction_id":"721","amount":"11.15","key_session":"[FILTERED]"}IVCA33721\"}" - -> "HTTP/1.1 200 \r\n" - -> "Strict-Transport-Security: max-age=31536000;includeSubDomains\r\n" - -> "X-Content-Type-Options: nosniff\r\n" - -> "X-XSS-Protection: 1; mode=block\r\n" - -> "Content-Type: text/html;charset=ISO-8859-1\r\n" - -> "Content-Length: 280\r\n" - -> "Date: Mon, 06 Sep 2021 19:02:08 GMT\r\n" - -> "Connection: close\r\n" - -> "Server: \r\n" - -> "Set-Cookie: UqZBpD3n=v1JocyJQ__9tu; Expires=Mon, 06-Sep-2021 19:03:39 GMT; Path=/; Secure; HttpOnly\r\n" - -> "\r\n" - response: {"folio_cdp":"095492915","auth":"929151","response":"approved","message":"0C- ","id_comercio":"147","reference":"721","amount":"11.15","time":"19:02:09 06:09:2021","operation":"Capture"}read 280 bytes - Conn close + <<~POST_SCRUBBED + starting SSL for wpy.mitec.com.mx:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 + <- "POST /ModuloUtilWS/activeCDP.htm HTTP/1.1\r\nContent-Type: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: wpy.mitec.com.mx\r\nContent-Length: 607\r\n\r\n" + <- "{\"payload\":\"{\"operation\":\"Authorize\",\"commerce_id\":\"147\",\"user\":\"IVCA33721\",\"apikey\":\"[FILTERED]\",\"testMode\":\"YES\",\"amount\":\"11.15\",\"currency\":\"MXN\",\"reference\":\"721\",\"transaction_id\":\"721\",\"installments\":1,\"card\":\"[FILTERED]\",\"expmonth\":9,\"expyear\":2025,\"cvv\":\"[FILTERED]\",\"name_client\":\"Pedro Flores Valdes\",\"email\":\"nadie@mit.test\",\"key_session\":\"[FILTERED]\"}IVCA33721\"}" + -> "HTTP/1.1 200 \r\n" + -> "Strict-Transport-Security: max-age=31536000;includeSubDomains\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "Content-Type: text/html;charset=ISO-8859-1\r\n" + -> "Content-Length: 320\r\n" + -> "Date: Mon, 06 Sep 2021 19:02:08 GMT\r\n" + -> "Connection: close\r\n" + -> "Server: \r\n" + -> "Set-Cookie: UqZBpD3n=v1I4cyJQ__N2M; Expires=Mon, 06-Sep-2021 19:03:38 GMT; Path=/; Secure; HttpOnly\r\n" + -> "\r\n" + reading 320 bytes... + -> "hl0spHqAAamtY47Vo+W+dZcpDyK8QRqpx/gWzIM1F3X1VFV/zNUcKCuqaSL6F4S7MqOGUMOC3BXIZYaS9TpJf6xsMYeRDyMpiv+sE0VpY2a4gULhLv1ztgGHgF3OpMjD8ucgLbd9FMA5OZjd8wlaqn46JCiYNcNIPV7hkHWNCqSWow+C+SSkWZeaa9YpNT3E6udixbog30/li1FcSI+Ti80EWBIdH3JDcQvjQbqecNb87JYad0EhgqL1o7ZEMehfZ2kW9FG6OXjGzWyhiWd2GEFKe8em4vEJxARFdXsaHe3tX0jqnF2gYOiFRclqFkbk" + read 320 bytes + + {\"folio_cdp\":\"095492846\",\"auth\":\"928468\",\"response\":\"approved\",\"message\":\"0C- Pago aprobado (test)\",\"id_comercio\":\"147\",\"reference\":\"721\",\"amount\":\"11.15\",\"time\":\"19:02:08 06:09:2021\",\"operation\":\"Authorize\"} + + {\"folio_cdp\":\"095492915\",\"auth\":\"929151\",\"response\":\"approved\",\"message\":\"0C- \",\"id_comercio\":\"147\",\"reference\":\"721\",\"amount\":\"11.15\",\"time\":\"19:02:09 06:09:2021\",\"operation\":\"Capture\"} + Conn close + opening connection to wpy.mitec.com.mx:443... + opened + starting SSL for wpy.mitec.com.mx:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 + <- "POST /ModuloUtilWS/activeCDP.htm HTTP/1.1\r\nContent-Type: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: wpy.mitec.com.mx\r\nContent-Length: 359\r\n\r\n" + <- "{\"payload\":\"{\"operation\":\"Capture\",\"commerce_id\":\"147\",\"user\":\"IVCA33721\",\"apikey\":\"[FILTERED]\",\"testMode\":\"YES\",\"transaction_id\":\"721\",\"amount\":\"11.15\",\"key_session\":\"[FILTERED]\"}IVCA33721\"}" + -> "HTTP/1.1 200 \r\n" + -> "Strict-Transport-Security: max-age=31536000;includeSubDomains\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "Content-Type: text/html;charset=ISO-8859-1\r\n" + -> "Content-Length: 280\r\n" + -> "Date: Mon, 06 Sep 2021 19:02:08 GMT\r\n" + -> "Connection: close\r\n" + -> "Server: \r\n" + -> "Set-Cookie: UqZBpD3n=v1JocyJQ__9tu; Expires=Mon, 06-Sep-2021 19:03:39 GMT; Path=/; Secure; HttpOnly\r\n" + -> "\r\n" + reading 280 bytes... + -> "BnuAgMOx9USBreICk027VY2ZqJA7xQcRT9Ytz8WpabDnqIglj43J/I03pKLtDlFrerKIAzhW1YCroDOS7mvtA5YnWezLstoOK0LbIcYqLzj1dCFW2zLb9ssTCxJa6ZmEQdzQdl8pyY4mC0QQ0JrOrsSA9QfX1XhkdcSVnsxQV1cEooL8/6EsVFCb6yVIMhVnGL6GRCc2J+rPigHsljLWRovgRKqFIURJjNWbfqepDRPG2hCNKsabM/lE2DFtKLMs4J5iwY9HiRbrAMG6BaGNiQ==" + read 280 bytes + + {\"folio_cdp\":\"095492846\",\"auth\":\"928468\",\"response\":\"approved\",\"message\":\"0C- Pago aprobado (test)\",\"id_comercio\":\"147\",\"reference\":\"721\",\"amount\":\"11.15\",\"time\":\"19:02:08 06:09:2021\",\"operation\":\"Authorize\"} + + {\"folio_cdp\":\"095492915\",\"auth\":\"929151\",\"response\":\"approved\",\"message\":\"0C- \",\"id_comercio\":\"147\",\"reference\":\"721\",\"amount\":\"11.15\",\"time\":\"19:02:09 06:09:2021\",\"operation\":\"Capture\"} + Conn close POST_SCRUBBED end end diff --git a/test/unit/gateways/monei_test.rb b/test/unit/gateways/monei_test.rb index 185eaad3292..005880124f1 100755 --- a/test/unit/gateways/monei_test.rb +++ b/test/unit/gateways/monei_test.rb @@ -157,6 +157,27 @@ def test_sending_cardholder_name end.respond_with(successful_purchase_response) end + def test_sending_browser_info + ip = '77.110.174.153' + user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36' + lang = 'en' + + @options.merge!({ + ip: ip, + user_agent: user_agent, + lang: lang + }) + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data)['sessionDetails'] + assert_equal ip, parsed['ip'] + assert_equal user_agent, parsed['userAgent'] + assert_equal lang, parsed['lang'] + end.respond_with(successful_purchase_response) + end + def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end diff --git a/test/unit/gateways/moneris_test.rb b/test/unit/gateways/moneris_test.rb index 5c7ee922fbc..4ab51538b20 100644 --- a/test/unit/gateways/moneris_test.rb +++ b/test/unit/gateways/moneris_test.rb @@ -36,35 +36,47 @@ def test_successful_purchase end def test_successful_mpi_cavv_purchase - @gateway.expects(:ssl_post).returns(successful_cavv_purchase_response) + options = @options.merge( + three_d_secure: { + version: '2', + cavv: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + eci: @fully_authenticated_eci, + three_ds_server_trans_id: 'd0f461f8-960f-40c9-a323-4e43a4e16aaa', + ds_transaction_id: '12345' + } + ) + + response = stub_comms do + @gateway.purchase(100, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/12345<\/ds_trans_id>/, data) + assert_match(/d0f461f8-960f-40c9-a323-4e43a4e16aaa<\/threeds_server_trans_id>/, data) + assert_match(/2<\/threeds_version>/, data) + end.respond_with(successful_cavv_purchase_response) - assert response = @gateway.purchase(100, @credit_card, - @options.merge( - three_d_secure: { - version: '2', - cavv: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', - eci: @fully_authenticated_eci, - three_ds_server_trans_id: 'd0f461f8-960f-40c9-a323-4e43a4e16aaa', - ds_transaction_id: '12345' - } - )) assert_success response assert_equal '69785-0_98;a131684dbecc1d89d9927c539ed3791b', response.authorization end def test_failed_mpi_cavv_purchase - @gateway.expects(:ssl_post).returns(failed_cavv_purchase_response) + options = @options.merge( + three_d_secure: { + version: '2', + cavv: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', + eci: @fully_authenticated_eci, + three_ds_server_trans_id: 'd0f461f8-960f-40c9-a323-4e43a4e16aaa', + ds_transaction_id: '12345' + } + ) + + response = stub_comms do + @gateway.purchase(100, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/12345<\/ds_trans_id>/, data) + assert_match(/d0f461f8-960f-40c9-a323-4e43a4e16aaa<\/threeds_server_trans_id>/, data) + assert_match(/2<\/threeds_version>/, data) + end.respond_with(failed_cavv_purchase_response) - assert response = @gateway.purchase(100, @credit_card, - @options.merge( - three_d_secure: { - version: '2', - cavv: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', - eci: @fully_authenticated_eci, - three_ds_server_trans_id: 'd0f461f8-960f-40c9-a323-4e43a4e16aaa', - ds_transaction_id: '12345' - } - )) assert_failure response assert_equal '69785-0_98;a131684dbecc1d89d9927c539ed3791b', response.authorization end @@ -128,9 +140,11 @@ def test_successful_subsequent_purchase_with_credential_on_file def test_successful_purchase_with_network_tokenization @gateway.expects(:ssl_post).returns(successful_purchase_network_tokenization) - @credit_card = network_tokenization_credit_card('4242424242424242', + @credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', - verification_value: nil) + verification_value: nil + ) assert response = @gateway.purchase(100, @credit_card, @options) assert_success response assert_equal '101965-0_10;0bbb277b543a17b6781243889a689573', response.authorization @@ -277,9 +291,11 @@ def test_successful_purchase_with_vault def test_successful_authorize_with_network_tokenization @gateway.expects(:ssl_post).returns(successful_authorization_network_tokenization) - @credit_card = network_tokenization_credit_card('4242424242424242', + @credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', - verification_value: nil) + verification_value: nil + ) assert response = @gateway.authorize(100, @credit_card, @options) assert_success response assert_equal '109232-0_10;d88d9f5f3472898832c54d6b5572757e', response.authorization diff --git a/test/unit/gateways/nmi_test.rb b/test/unit/gateways/nmi_test.rb index 7cbf9ef6510..3736b25b6bb 100644 --- a/test/unit/gateways/nmi_test.rb +++ b/test/unit/gateways/nmi_test.rb @@ -125,6 +125,70 @@ def test_purchase_with_options assert_success response end + def test_purchase_with_surcharge + options = @transaction_options.merge({ surcharge: '1.00' }) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + test_transaction_options(data) + + assert_match(/surcharge=1.00/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_purchase_with_shipping_fields + options = @transaction_options.merge({ shipping_address: shipping_address }) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + test_transaction_options(data) + + assert_match(/shipping_firstname=Jon/, data) + assert_match(/shipping_lastname=Smith/, data) + assert_match(/shipping_address1=123\+Your\+Street/, data) + assert_match(/shipping_address2=Apt\+2/, data) + assert_match(/shipping_city=Toronto/, data) + assert_match(/shipping_state=ON/, data) + assert_match(/shipping_country=CA/, data) + assert_match(/shipping_zip=K2C3N7/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_purchase_with_shipping_fields_omits_blank_name + options = @transaction_options.merge({ shipping_address: shipping_address(name: nil) }) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + test_transaction_options(data) + + refute_match(/shipping_firstname/, data) + refute_match(/shipping_lastname/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_purchase_with_shipping_email + options = @transaction_options.merge({ shipping_address: shipping_address, shipping_email: 'test@example.com' }) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + test_transaction_options(data) + + assert_match(/shipping_email=test%40example\.com/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_failed_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card) @@ -524,8 +588,7 @@ def test_blank_cvv_not_sent end def test_supported_countries - assert_equal 1, - (['US'] | NmiGateway.supported_countries).size + assert_equal 1, (['US'] | NmiGateway.supported_countries).size end def test_supported_card_types @@ -760,8 +823,7 @@ def test_verify(options = {}) assert_match(/payment=creditcard/, data) assert_match(/ccnumber=#{@credit_card.number}/, data) assert_match(/cvv=#{@credit_card.verification_value}/, data) - assert_match(/ccexp=#{sprintf("%.2i", @credit_card.month)}#{@credit_card.year.to_s[-2..-1]}/, - data) + assert_match(/ccexp=#{sprintf("%.2i", @credit_card.month)}#{@credit_card.year.to_s[-2..-1]}/, data) test_level3_options(data) if options.any? end.respond_with(successful_validate_response) diff --git a/test/unit/gateways/ogone_test.rb b/test/unit/gateways/ogone_test.rb index d6f42c1eccc..906ea36c184 100644 --- a/test/unit/gateways/ogone_test.rb +++ b/test/unit/gateways/ogone_test.rb @@ -41,6 +41,10 @@ def teardown Base.mode = :test end + def test_should_have_homepage_url + assert_equal 'https://www.ingenico.com/login/ogone/', OgoneGateway.homepage_url + end + def test_successful_purchase @gateway.expects(:add_pair).at_least(1) @gateway.expects(:add_pair).with(anything, 'ECI', '7') diff --git a/test/unit/gateways/openpay_test.rb b/test/unit/gateways/openpay_test.rb index 9407048819b..1cc39b539ec 100644 --- a/test/unit/gateways/openpay_test.rb +++ b/test/unit/gateways/openpay_test.rb @@ -31,6 +31,40 @@ def test_successful_purchase assert response.test? end + def test_successful_purchase_with_mexico_url + gateway = OpenpayGateway.new( + key: 'key', + merchant_id: 'merchant_id', + merchant_country: 'MX' + ) + + gateway.expects(:ssl_request).returns(successful_purchase_response) + assert_equal gateway.gateway_url, OpenpayGateway.mx_test_url + assert response = gateway.purchase(@amount, @credit_card, @options) + assert_instance_of Response, response + assert_success response + + assert_equal 'tay1mauq3re4iuuk8bm4', response.authorization + assert response.test? + end + + def test_default_url_when_merchant_country_is_not_present + gateway = OpenpayGateway.new( + key: 'key', + merchant_id: 'merchant_id' + ) + assert_equal 'https://sandbox-api.openpay.co/v1/', gateway.gateway_url + end + + def test_set_mexico_url_using_merchant_country_flag + gateway = OpenpayGateway.new( + key: 'key', + merchant_id: 'merchant_id', + merchant_country: 'MX' + ) + assert_equal 'https://sandbox-api.openpay.mx/v1/', gateway.gateway_url + end + def test_unsuccessful_request @gateway.expects(:ssl_request).returns(failed_purchase_response) diff --git a/test/unit/gateways/opp_test.rb b/test/unit/gateways/opp_test.rb index 6dfcf179097..ec7af16f8bf 100644 --- a/test/unit/gateways/opp_test.rb +++ b/test/unit/gateways/opp_test.rb @@ -202,7 +202,8 @@ def post_scrubbed end def successful_response(type, id) - OppMockResponse.new(200, + OppMockResponse.new( + 200, JSON.generate({ 'id' => id, 'paymentType' => type, @@ -224,11 +225,13 @@ def successful_response(type, id) 'buildNumber' => '20150618-111601.r185004.opp-tags-20150618_stage', 'timestamp' => '2015-06-20 19:31:01+0000', 'ndc' => '8a8294174b7ecb28014b9699220015ca_4453edbc001f405da557c05cb3c3add9' - })) + }) + ) end def successful_store_response(id) - OppMockResponse.new(200, + OppMockResponse.new( + 200, JSON.generate({ 'id' => id, 'result' => { @@ -245,11 +248,13 @@ def successful_store_response(id) 'buildNumber' => '20150618-111601.r185004.opp-tags-20150618_stage', 'timestamp' => '2015-06-20 19:31:01+0000', 'ndc' => '8a8294174b7ecb28014b9699220015ca_4453edbc001f405da557c05cb3c3add9' - })) + }) + ) end def failed_response(type, id, code = '100.100.101') - OppMockResponse.new(400, + OppMockResponse.new( + 400, JSON.generate({ 'id' => id, 'paymentType' => type, @@ -268,11 +273,13 @@ def failed_response(type, id, code = '100.100.101') 'buildNumber' => '20150618-111601.r185004.opp-tags-20150618_stage', 'timestamp' => '2015-06-20 20:40:26+0000', 'ndc' => '8a8294174b7ecb28014b9699220015ca_5200332e7d664412a84ed5f4777b3c7d' - })) + }) + ) end def failed_store_response(id, code = '100.100.101') - OppMockResponse.new(400, + OppMockResponse.new( + 400, JSON.generate({ 'id' => id, 'result' => { @@ -289,7 +296,8 @@ def failed_store_response(id, code = '100.100.101') 'buildNumber' => '20150618-111601.r185004.opp-tags-20150618_stage', 'timestamp' => '2015-06-20 20:40:26+0000', 'ndc' => '8a8294174b7ecb28014b9699220015ca_5200332e7d664412a84ed5f4777b3c7d' - })) + }) + ) end class OppMockResponse diff --git a/test/unit/gateways/orbital_test.rb b/test/unit/gateways/orbital_test.rb index c91012a9390..1765228b08e 100644 --- a/test/unit/gateways/orbital_test.rb +++ b/test/unit/gateways/orbital_test.rb @@ -10,7 +10,7 @@ def setup @gateway = ActiveMerchant::Billing::OrbitalGateway.new( login: 'login', password: 'password', - merchant_id: 'merchant_id' + merchant_id: 'test12' ) @customer_ref_num = 'ABC' @credit_card = credit_card('4556761029983886') @@ -102,6 +102,19 @@ def setup ds_transaction_id: '97267598FAE648F28083C23433990FBC' } } + + @google_pay_card = network_tokenization_credit_card( + '4777777777777778', + payment_cryptogram: 'BwAQCFVQdwEAABNZI1B3EGLyGC8=', + verification_value: '987', + source: :google_pay, + brand: 'visa', + eci: '5' + ) + end + + def test_supports_network_tokenization + assert_true @gateway.supports_network_tokenization? end def test_supports_network_tokenization @@ -127,6 +140,16 @@ def test_successful_purchase_with_echeck assert_equal '5F8E8BEE7299FD339A38F70CFF6E5D010EF55498;9baedc697f2cf06457de78', response.authorization end + def test_successful_purchase_with_commercial_echeck + commercial_echeck = check(account_number: '072403004', account_type: 'checking', account_holder_type: 'business', routing_number: '072403004') + + stub_comms do + @gateway.purchase(50, commercial_echeck, order_id: '9baedc697f2cf06457de78') + end.check_request do |_endpoint, data, _headers| + assert_match %{X}, data + end.respond_with(successful_purchase_with_echeck_response) + end + def test_failed_purchase_with_echeck @gateway.expects(:ssl_post).returns(failed_echeck_for_invalid_routing_response) @@ -222,6 +245,22 @@ def test_line_items_data end.respond_with(successful_purchase_response) end + def test_payment_action_ind_field + stub_comms do + @gateway.purchase(50, credit_card, @options.merge(payment_action_ind: 'P')) + end.check_request do |_endpoint, data, _headers| + assert_match %{P}, data + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_secondary_url + stub_comms do + @gateway.purchase(50, credit_card, @options.merge(use_secondary_url: 'true')) + end.check_request do |endpoint, _data, _headers| + assert endpoint.include? 'orbitalvar2' + end.respond_with(successful_purchase_response) + end + def test_network_tokenization_credit_card_data stub_comms do @gateway.purchase(50, network_tokenization_credit_card(nil, eci: '5', transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA='), @options) @@ -383,6 +422,24 @@ def test_three_d_secure_data_on_american_express_authorization end.respond_with(successful_purchase_response) end + def test_three_d_secure_data_on_discover_purchase + stub_comms do + @gateway.purchase(50, credit_card(nil, brand: 'discover'), @options.merge(@three_d_secure_options)) + end.check_request do |_endpoint, data, _headers| + assert_match %{5}, data + assert_match %{TESTCAVV}, data + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_discover_authorization + stub_comms do + @gateway.authorize(50, credit_card(nil, brand: 'discover'), @options.merge(@three_d_secure_options)) + end.check_request do |_endpoint, data, _headers| + assert_match %{5}, data + assert_match %{TESTCAVV}, data + end.respond_with(successful_purchase_response) + end + def test_currency_exponents stub_comms do @gateway.purchase(50, credit_card, order_id: '1') @@ -505,9 +562,7 @@ def test_truncates_address end def test_truncates_name - card = credit_card('4242424242424242', - first_name: 'John', - last_name: 'Jacob Jingleheimer Smith-Jones') + card = credit_card('4242424242424242', first_name: 'John', last_name: 'Jacob Jingleheimer Smith-Jones') response = stub_comms do @gateway.purchase(50, card, order_id: 1, billing_address: address) @@ -596,8 +651,7 @@ def test_address_format response = stub_comms do assert_deprecation_warning do - @gateway.add_customer_profile(credit_card, - billing_address: address_with_invalid_chars) + @gateway.add_customer_profile(credit_card, billing_address: address_with_invalid_chars) end end.check_request do |_endpoint, data, _headers| assert_match(/456 Main Street1<\/CardSecValInd>}, data assert_match %r{123<\/CardSecVal>}, data end.respond_with(successful_purchase_response) + + stub_comms do + @gateway.purchase(50, discover, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r{1<\/CardSecValInd>}, data + assert_match %r{123<\/CardSecVal>}, data + end.respond_with(successful_purchase_response) + + stub_comms do + @gateway.purchase(50, diners_club, @options) + end.check_request do |_endpoint, data, _headers| + assert_match %r{1<\/CardSecValInd>}, data + assert_match %r{123<\/CardSecVal>}, data + end.respond_with(successful_purchase_response) + end + + def test_cvv_indicator_absent_for_mastercard + mastercard = credit_card('4556761029983886', brand: 'master') + + stub_comms do + @gateway.purchase(50, mastercard, @options) + end.check_request do |_endpoint, data, _headers| + assert_no_match %r{}, data + assert_match %r{123<\/CardSecVal>}, data + end.respond_with(successful_purchase_response) end def test_cvv_indicator_absent_for_recurring diff --git a/test/unit/gateways/pac_net_raven_test.rb b/test/unit/gateways/pac_net_raven_test.rb index e98214fe6bd..d206b211448 100644 --- a/test/unit/gateways/pac_net_raven_test.rb +++ b/test/unit/gateways/pac_net_raven_test.rb @@ -337,7 +337,8 @@ def test_post_data @gateway.stubs(request_id: 'wouykiikdvqbwwxueppby') @gateway.stubs(timestamp: '2013-10-08T14:31:54.Z') - assert_equal "PymtType=cc_preauth&RAPIVersion=2&UserName=user&Timestamp=2013-10-08T14%3A31%3A54.Z&RequestID=wouykiikdvqbwwxueppby&Signature=7794efc8c0d39f0983edc10f778e6143ba13531d&CardNumber=4242424242424242&Expiry=09#{@credit_card.year.to_s[-2..-1]}&CVV2=123&Currency=USD&BillingStreetAddressLineOne=Address+1&BillingStreetAddressLineFour=Address+2&BillingPostalCode=ZIP123", + assert_equal( + "PymtType=cc_preauth&RAPIVersion=2&UserName=user&Timestamp=2013-10-08T14%3A31%3A54.Z&RequestID=wouykiikdvqbwwxueppby&Signature=7794efc8c0d39f0983edc10f778e6143ba13531d&CardNumber=4242424242424242&Expiry=09#{@credit_card.year.to_s[-2..-1]}&CVV2=123&Currency=USD&BillingStreetAddressLineOne=Address+1&BillingStreetAddressLineFour=Address+2&BillingPostalCode=ZIP123", @gateway.send(:post_data, 'cc_preauth', { 'CardNumber' => @credit_card.number, 'Expiry' => @gateway.send(:expdate, @credit_card), @@ -347,6 +348,7 @@ def test_post_data 'BillingStreetAddressLineFour' => 'Address 2', 'BillingPostalCode' => 'ZIP123' }) + ) end def test_signature_for_cc_preauth_action diff --git a/test/unit/gateways/pay_trace_test.rb b/test/unit/gateways/pay_trace_test.rb index bc48625d8f4..09f13807e83 100644 --- a/test/unit/gateways/pay_trace_test.rb +++ b/test/unit/gateways/pay_trace_test.rb @@ -16,6 +16,7 @@ class PayTraceTest < Test::Unit::TestCase def setup @gateway = PayTraceGateway.new(username: 'username', password: 'password', integrator_id: 'uniqueintegrator') @credit_card = credit_card + @echeck = check(account_number: '123456', routing_number: '325070760') @amount = 100 @options = { @@ -31,6 +32,43 @@ def test_successful_purchase assert_equal 392483066, response.authorization end + def test_successful_purchase_with_ach + response = stub_comms(@gateway) do + @gateway.purchase(@amount, @echeck, @options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/sale/by_account' + assert_equal request['amount'], '1.00' + assert_equal request['check']['account_number'], @echeck.account_number + assert_equal request['check']['routing_number'], @echeck.routing_number + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + assert_equal request['billing_address']['name'], @options[:billing_address][:name] + assert_equal request['billing_address']['street_address'], @options[:billing_address][:address1] + assert_equal request['billing_address']['city'], @options[:billing_address][:city] + assert_equal request['billing_address']['state'], @options[:billing_address][:state] + assert_equal request['billing_address']['zip'], @options[:billing_address][:zip] + end.respond_with(successful_ach_processing_response) + + assert_success response + assert_equal response.message, 'Your check was successfully processed.' + end + + def test_successful_purchase_by_customer_with_ach + customer_id = 'customerId121' + response = stub_comms(@gateway) do + @gateway.purchase(@amount, customer_id, { check_transaction: 'true' }) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/sale/by_customer' + assert_equal request['amount'], '1.00' + assert_equal request['customer_id'], customer_id + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + end.respond_with(successful_ach_processing_response) + + assert_success response + assert_equal response.message, 'Your check was successfully processed.' + end + def test_successful_purchase_with_level_3_data @gateway.expects(:ssl_post).times(2).returns(successful_purchase_response).then.returns(successful_level_3_response) @@ -105,6 +143,22 @@ def test_failed_purchase assert_equal PayTraceGateway::STANDARD_ERROR_CODE[:declined], response.error_code end + def test_failed_ach_processing + @gateway.expects(:ssl_post).returns(failed_ach_processing_response) + + response = @gateway.purchase(@amount, @echeck, @options) + assert_failure response + assert_equal response.message, 'Your check was NOT successfully processed. ' + end + + def test_failed_bad_request_ach_processing + @gateway.expects(:ssl_post).returns(failed_bad_request_ach_processing_response) + + response = @gateway.authorize(@amount, @echeck, @options) + assert_failure response + assert_include response.message, 'Please provide a valid Checking Account Number.' + end + def test_successful_authorize @gateway.expects(:ssl_post).returns(successful_authorize_response) @@ -113,6 +167,43 @@ def test_successful_authorize assert_equal true, response.success? end + def test_successful_authorize_with_ach + response = stub_comms(@gateway) do + @gateway.authorize(@amount, @echeck, @options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/hold/by_account' + assert_equal request['amount'], '1.00' + assert_equal request['check']['account_number'], @echeck.account_number + assert_equal request['check']['routing_number'], @echeck.routing_number + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + assert_equal request['billing_address']['name'], @options[:billing_address][:name] + assert_equal request['billing_address']['street_address'], @options[:billing_address][:address1] + assert_equal request['billing_address']['city'], @options[:billing_address][:city] + assert_equal request['billing_address']['state'], @options[:billing_address][:state] + assert_equal request['billing_address']['zip'], @options[:billing_address][:zip] + end.respond_with(successful_ach_processing_response) + + assert_success response + assert_equal response.message, 'Your check was successfully processed.' + end + + def test_successful_authorize_by_customer_with_ach + customer_id = 'customerId121' + response = stub_comms(@gateway) do + @gateway.authorize(@amount, customer_id, { check_transaction: 'true' }) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/hold/by_customer' + assert_equal request['amount'], '1.00' + assert_equal request['customer_id'], customer_id + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + end.respond_with(successful_ach_processing_response) + + assert_success response + assert_equal response.message, 'Your check was successfully processed.' + end + def test_failed_authorize @gateway.expects(:ssl_post).returns(failed_authorize_response) @@ -130,6 +221,22 @@ def test_successful_capture assert_equal 'Your transaction was successfully captured.', response.message end + def test_successful_capture_with_ach + check_transaction_id = 9981615 + response = stub_comms(@gateway) do + @gateway.capture(@amount, check_transaction_id, { check_transaction: 'true' }) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/manage/fund' + assert_equal request['amount'], '1.00' + assert_equal request['check_transaction_id'], check_transaction_id + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + end.respond_with(successful_ach_capture_void_response) + + assert_success response + assert_equal response.message, 'Your check was successfully managed.' + end + def test_successful_level_3_data_field_mapping authorization = 123456789 options = { @@ -155,6 +262,14 @@ def test_failed_capture assert_equal 'Errors- code:58, message:["Please provide a valid Transaction ID."]', response.message end + def test_failed_capture_void_response + @gateway.expects(:ssl_post).returns(failed_ach_capture_void_response) + + response = @gateway.capture(@amount, @echeck, @options) + assert_failure response + assert_include response.message, 'The Check ID that you have provided was not found in the PayTrace records. It may already be voided or settled.' + end + def test_successful_refund transaction_id = 105968532 @gateway.expects(:ssl_post).returns(successful_refund_response) @@ -164,6 +279,22 @@ def test_successful_refund assert_equal 'Your transaction successfully refunded.', response.message end + def test_successful_refund_with_ach + check_transaction_id = 9981615 + response = stub_comms(@gateway) do + @gateway.refund(@amount, check_transaction_id, { check_transaction: 'true' }) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/refund/by_transaction' + assert_equal request['amount'], '1.00' + assert_equal request['check_transaction_id'], check_transaction_id + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + end.respond_with(successful_ach_refund_response) + + assert_success response + assert_equal response.message, 'Your check was successfully refunded.' + end + def test_failed_refund @gateway.expects(:ssl_post).returns(failed_refund_response) @@ -172,6 +303,14 @@ def test_failed_refund assert_equal 'Errors- code:981, message:["Log in failed for insufficient permissions."]', response.message end + def test_failed_refund_response + @gateway.expects(:ssl_post).returns(failed_ach_refund_response) + + response = @gateway.refund(@amount, @echeck, @options) + assert_failure response + assert_include response.message, 'The Check ID that you provided was not found in the PayTrace record.' + end + def test_successful_void transaction_id = 105968551 @gateway.expects(:ssl_post).returns(successful_void_response) @@ -181,6 +320,21 @@ def test_successful_void assert_equal 'Your transaction was successfully voided.', void.message end + def test_successful_void_with_ach + check_transaction_id = 9981615 + response = stub_comms(@gateway) do + @gateway.void(check_transaction_id, { check_transaction: 'true' }) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_include endpoint, 'checks/manage/void' + assert_equal request['check_transaction_id'], check_transaction_id + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + end.respond_with(successful_ach_capture_void_response) + + assert_success response + assert_equal response.message, 'Your check was successfully managed.' + end + def test_failed_void @gateway.expects(:ssl_post).returns(failed_void_response) @@ -377,4 +531,32 @@ def successful_create_customer_response def failed_customer_creation_response '{"success":false,"response_code":1,"status_message":"One or more errors has occurred.","errors":{"171":["Please provide a unique customer ID."]},"masked_card_number":"xxxxxxxxxxxx5439"}' end + + def successful_ach_processing_response + '{ "success":true, "response_code":120, "status_message":"Your check was successfully processed.", "check_transaction_id":981619 }' + end + + def successful_ach_capture_void_response + '{ "success":true, "response_code":124, "status_message":"Your check was successfully managed.", "check_transaction_id":9981614 }' + end + + def successful_ach_refund_response + '{ "success":true, "response_code":122, "status_message":"Your check was successfully refunded.", "check_transaction_id":9981632 }' + end + + def failed_ach_processing_response + '{ "success":false, "response_code":125, "status_message":"Your check was NOT successfully processed.", "check_transaction_id":981610, "ach_code":0, "ach_message":"" }' + end + + def failed_bad_request_ach_processing_response + '{ "success":false, "response_code":1, "status_message":"One or more errors has occurred.", "errors":{ "30":[ "Customer ID, customerid123, was not found or is incomplete." ], "45":[ "Please provide a valid Checking Account Number." ], "46":[ "Please provide a valid Transit Routing Number." ] } }' + end + + def failed_ach_refund_response + '{ "success":false, "response_code":1, "status_message":"One or more errors has occurred.", "errors":{ "80":[ "The Check ID that you provided was not found in the PayTrace record." ] } }' + end + + def failed_ach_capture_void_response + '{ "success":false, "response_code":1, "status_message":"One or more errors has occurred.", "errors":{ "80":[ "The Check ID that you have provided was not found in the PayTrace records. It may already be voided or settled." ] } }' + end end diff --git a/test/unit/gateways/paybox_direct_test.rb b/test/unit/gateways/paybox_direct_test.rb index a10fab948bc..0676dc82ef9 100644 --- a/test/unit/gateways/paybox_direct_test.rb +++ b/test/unit/gateways/paybox_direct_test.rb @@ -9,8 +9,7 @@ def setup password: 'p' ) - @credit_card = credit_card('1111222233334444', - brand: 'visa') + @credit_card = credit_card('1111222233334444', brand: 'visa') @amount = 100 @options = { diff --git a/test/unit/gateways/payeezy_test.rb b/test/unit/gateways/payeezy_test.rb index e4fbac47d53..bb92e56e002 100644 --- a/test/unit/gateways/payeezy_test.rb +++ b/test/unit/gateways/payeezy_test.rb @@ -16,7 +16,7 @@ def setup ta_token: '123' } @options_stored_credentials = { - cardbrand_original_transaction_id: 'abc123', + cardbrand_original_transaction_id: 'original_transaction_id_abc123', sequence: 'FIRST', is_scheduled: true, initiator: 'MERCHANT', @@ -24,7 +24,7 @@ def setup } @options_standardized_stored_credentials = { stored_credential: { - network_transaction_id: 'abc123', + network_transaction_id: 'stored_credential_abc123', initial_transaction: false, reason_type: 'recurring', initiator: 'cardholder' @@ -46,6 +46,25 @@ def setup merchant_contact_info: '8885551212' } } + @apple_pay_card = network_tokenization_credit_card( + '4761209980011439', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: '2022', + eci: 5, + source: :apple_pay, + verification_value: 569 + ) + @apple_pay_card_amex = network_tokenization_credit_card( + '373953192351004', + brand: 'american_express', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: Time.now.year + 1, + eci: 5, + source: :apple_pay, + verification_value: 569 + ) end def test_invalid_credentials @@ -87,6 +106,52 @@ def test_successful_purchase assert_equal 'Transaction Normal - Approved', response.message end + def test_successful_purchase_with_apple_pay + stub_comms do + @gateway.purchase(@amount, @apple_pay_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['method'], '3DS' + assert_equal request['3DS']['type'], 'D' + assert_equal request['3DS']['wallet_provider_id'], 'APPLE_PAY' + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_apple_pay_no_cryptogram + @apple_pay_card.payment_cryptogram = '' + @apple_pay_card.eci = nil + stub_comms do + @gateway.purchase(@amount, @apple_pay_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['eci_indicator'], '5' + assert_nil request['3DS']['xid'] + assert_nil request['3DS']['cavv'] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_apple_pay_amex + stub_comms do + @gateway.purchase(@amount, @apple_pay_card_amex, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert request['3DS']['cavv'], @apple_pay_card_amex.payment_cryptogram + assert_nil request['3DS']['xid'] + end.respond_with(successful_purchase_response) + end + + def test_failed_purchase_no_name + @apple_pay_card.first_name = nil + @apple_pay_card.last_name = nil + @options[:billing_address] = nil + stub_comms do + @gateway.purchase(@amount, @apple_pay_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal nil, request['cardholder_name'] + end.respond_with(failed_purchase_no_name_response) + end + def test_successful_store response = stub_comms(@gateway, :ssl_request) do @gateway.store(@credit_card, @options.merge(js_security_key: 'js-f4c4b54f08d6c44c8cad3ea80bbf92c4f4c4b54f08d6c44c')) @@ -160,7 +225,8 @@ def test_successful_purchase_with_stored_credentials response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(@options_stored_credentials)) end.check_request do |_endpoint, data, _headers| - assert_match(/stored_credentials/, data) + stored_credentials = JSON.parse(data)['stored_credentials']['cardbrand_original_transaction_id'] + assert_equal stored_credentials, 'original_transaction_id_abc123' end.respond_with(successful_purchase_stored_credentials_response) assert_success response @@ -172,7 +238,38 @@ def test_successful_purchase_with_standardized_stored_credentials response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(@options_standardized_stored_credentials)) end.check_request do |_endpoint, data, _headers| - assert_match(/stored_credentials/, data) + stored_credentials = JSON.parse(data)['stored_credentials']['cardbrand_original_transaction_id'] + assert_equal stored_credentials, 'stored_credential_abc123' + end.respond_with(successful_purchase_stored_credentials_response) + + assert_success response + assert response.test? + assert_equal 'Transaction Normal - Approved', response.message + end + + def test_successful_purchase_with__stored_credential_and_cardbrand_original_transaction_id + options = @options_standardized_stored_credentials.merge!(cardbrand_original_transaction_id: 'original_transaction_id_abc123') + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(options)) + end.check_request do |_endpoint, data, _headers| + stored_credentials = JSON.parse(data)['stored_credentials']['cardbrand_original_transaction_id'] + assert_equal stored_credentials, 'original_transaction_id_abc123' + end.respond_with(successful_purchase_stored_credentials_response) + + assert_success response + assert response.test? + assert_equal 'Transaction Normal - Approved', response.message + end + + def test_successful_purchase_with_no_ntid + @options_standardized_stored_credentials[:stored_credential].delete(:network_transaction_id) + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(@options_standardized_stored_credentials)) + end.check_request do |_endpoint, data, _headers| + stored_credentials = JSON.parse(data)['stored_credentials'] + assert_equal stored_credentials.include?(:cardbrand_original_transaction_id), false end.respond_with(successful_purchase_stored_credentials_response) assert_success response @@ -408,6 +505,10 @@ def test_scrub_echeck assert_equal @gateway.scrub(pre_scrubbed_echeck), post_scrubbed_echeck end + def test_scrub_network_token + assert_equal @gateway.scrub(pre_scrubbed_network_token), post_scrubbed_network_token + end + private def pre_scrubbed @@ -588,6 +689,94 @@ def post_scrubbed_store TRANSCRIPT end + def pre_scrubbed_network_token + <<~TRANSCRIPT + opening connection to api-cert.payeezy.com:443... + opened + starting SSL for api-cert.payeezy.com:443... + SSL established + <- "POST /v1/transactions HTTP/1.1\r\nContent-Type: application/json\r\nApikey: oKB61AAxbN3xwC6gVAH3dp58FmioHSAT\r\nToken: fdoa-a480ce8951daa73262734cf102641994c1e55e7cdf4c02b6\r\nNonce: 2713241561.4909368\r\nTimestamp: 1668784714406\r\nAuthorization: NDU2ZWRiNmUwMmUxNGMwOGIwYjMxYTAxMDkzZDcwNWNhM2Y0ODExNmRmMTNjNDVjMTFhODMyNTg4NDdiNzZiNw==\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: api-cert.payeezy.com\r\nContent-Length: 462\r\n\r\n" + <- "{\"transaction_type\":\"purchase\",\"merchant_ref\":null,\"3DS\":{\"type\":\"D\",\"cardholder_name\":\"Longbob\",\"card_number\":\"4761209980011439\",\"exp_date\":\"1122\",\"cvv\":569,\"xid\":\"YwAAAAAABaYcCMX/OhNRQAAAAAA=\",\"cavv\":\"YwAAAAAABaYcCMX/OhNRQAAAAAA=\",\"wallet_provider_id\":\"APPLE_PAY\"},\"method\":\"3DS\",\"eci_indicator\":5,\"billing_address\":{\"street\":\"456 My Street\",\"city\":\"Ottawa\",\"state_province\":\"ON\",\"zip_postal_code\":\"K1C2N6\",\"country\":\"CA\"},\"currency_code\":\"USD\",\"amount\":\"100\"}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Fri, 18 Nov 2022 15:18:35 GMT\r\n" + -> "Content-Type: application/json;charset=UTF-8\r\n" + -> "Connection: close\r\n" + -> "X-Backside-Transport: OK OK,OK OK\r\n" + -> "Content-Language: en-US\r\n" + -> "X-Global-Transaction-ID: 7f41427d6377a24aa50b34df\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "X-Xss-Protection: 1; mode=block\r\n" + -> "Cache-Control: no-store, no-cache, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Referrer-Policy: strict-origin\r\n" + -> "Feature-Policy: vibrate 'self'\r\n" + -> "Content-Security-Policy: default-src 'none'; frame-ancestors 'self'; script-src 'unsafe-inline' 'self' *.googleapis.com *.klarna.com *.masterpass.com *.mastercard.com *.newrelic.com *.npci.org.in *.nr-data.net *.google-analytics.com *.google.com *.getsitecontrol.com *.gstatic.com *.kxcdn.com 'strict-dynamic' 'nonce-6f62fa22a79de4c553d2bbde' 'unsafe-eval' 'unsafe-inline'; connect-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self';\r\n" + -> "Access-Control-Allow-Origin: http://localhost:8080\r\n" + -> "Access-Control-Request-Headers: origin, x-requested-with, accept, content-type\r\n" + -> "Access-Control-Max-Age: 3628800\r\n" + -> "Access-Control-Allow-Methods: GET, PUT, POST, DELETE\r\n" + -> "Access-Control-Allow-Headers: Content-Type, apikey, token\r\n" + -> "Via: 1.1 dca1-bit16021\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "\r\n" + -> "249\r\n" + reading 585 bytes... + -> "{\"correlation_id\":\"134.6878471461658\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET188163\",\"transaction_tag\":\"10032826722\",\"method\":\"3ds\",\"amount\":\"100\",\"currency\":\"USD\",\"avs\":\"4\",\"cvv2\":\"U\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"9324008290401439\"}},\"card\":{\"type\":\"VISA\",\"cardholder_name\":\"Longbob\",\"card_number\":\"1439\",\"exp_date\":\"1122\"},\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"eCommerce_flag\":\"5\",\"retrieval_ref_no\":\"221118\"}" + read 585 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + TRANSCRIPT + end + + def post_scrubbed_network_token + <<~TRANSCRIPT + opening connection to api-cert.payeezy.com:443... + opened + starting SSL for api-cert.payeezy.com:443... + SSL established + <- "POST /v1/transactions HTTP/1.1\r\nContent-Type: application/json\r\nApikey: [FILTERED]\r\nToken: [FILTERED]\r\nNonce: 2713241561.4909368\r\nTimestamp: 1668784714406\r\nAuthorization: NDU2ZWRiNmUwMmUxNGMwOGIwYjMxYTAxMDkzZDcwNWNhM2Y0ODExNmRmMTNjNDVjMTFhODMyNTg4NDdiNzZiNw==\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: api-cert.payeezy.com\r\nContent-Length: 462\r\n\r\n" + <- "{\"transaction_type\":\"purchase\",\"merchant_ref\":null,\"3DS\":{\"type\":\"D\",\"cardholder_name\":\"Longbob\",\"card_number\":\"[FILTERED]\",\"exp_date\":\"1122\",\"cvv\":[FILTERED],\"xid\":[FILTERED],\"cavv\":[FILTERED],\"wallet_provider_id\":\"APPLE_PAY\"},\"method\":\"3DS\",\"eci_indicator\":5,\"billing_address\":{\"street\":\"456 My Street\",\"city\":\"Ottawa\",\"state_province\":\"ON\",\"zip_postal_code\":\"K1C2N6\",\"country\":\"CA\"},\"currency_code\":\"USD\",\"amount\":\"100\"}" + -> "HTTP/1.1 201 Created\r\n" + -> "Date: Fri, 18 Nov 2022 15:18:35 GMT\r\n" + -> "Content-Type: application/json;charset=UTF-8\r\n" + -> "Connection: close\r\n" + -> "X-Backside-Transport: OK OK,OK OK\r\n" + -> "Content-Language: en-US\r\n" + -> "X-Global-Transaction-ID: 7f41427d6377a24aa50b34df\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "X-Xss-Protection: 1; mode=block\r\n" + -> "Cache-Control: no-store, no-cache, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Frame-Options: SAMEORIGIN\r\n" + -> "Referrer-Policy: strict-origin\r\n" + -> "Feature-Policy: vibrate 'self'\r\n" + -> "Content-Security-Policy: default-src 'none'; frame-ancestors 'self'; script-src 'unsafe-inline' 'self' *.googleapis.com *.klarna.com *.masterpass.com *.mastercard.com *.newrelic.com *.npci.org.in *.nr-data.net *.google-analytics.com *.google.com *.getsitecontrol.com *.gstatic.com *.kxcdn.com 'strict-dynamic' 'nonce-6f62fa22a79de4c553d2bbde' 'unsafe-eval' 'unsafe-inline'; connect-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self';\r\n" + -> "Access-Control-Allow-Origin: http://localhost:8080\r\n" + -> "Access-Control-Request-Headers: origin, x-requested-with, accept, content-type\r\n" + -> "Access-Control-Max-Age: 3628800\r\n" + -> "Access-Control-Allow-Methods: GET, PUT, POST, DELETE\r\n" + -> "Access-Control-Allow-Headers: Content-Type, apikey, token\r\n" + -> "Via: 1.1 dca1-bit16021\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "\r\n" + -> "249\r\n" + reading 585 bytes... + -> "{\"correlation_id\":\"134.6878471461658\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET188163\",\"transaction_tag\":\"10032826722\",\"method\":\"3ds\",\"amount\":\"100\",\"currency\":\"USD\",\"avs\":\"4\",\"cvv2\":\"U\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"9324008290401439\"}},\"card\":{\"type\":\"VISA\",\"cardholder_name\":\"Longbob\",\"card_number\":\"[FILTERED]\",\"exp_date\":\"1122\"},\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"eCommerce_flag\":\"5\",\"retrieval_ref_no\":\"221118\"}" + read 585 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + TRANSCRIPT + end + def successful_purchase_response <<~RESPONSE {\"method\":\"credit_card\",\"amount\":\"1\",\"currency\":\"USD\",\"avs\":\"4\",\"card\":{\"type\":\"Visa\",\"cardholder_name\":\"Bobsen 995\",\"card_number\":\"4242\",\"exp_date\":\"0816\"},\"token\":{\"token_type\":\"transarmor\",\"token_data\":{\"value\":\"0152552999534242\"}},\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET114541\",\"transaction_tag\":\"55083431\",\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"correlation_id\":\"124.1433862672836\"} @@ -654,7 +843,7 @@ def failed_purchase_response body_exist: true message: RESPONSE - YAML.safe_load(yamlexcep, ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) end def failed_purchase_response_for_insufficient_funds @@ -697,6 +886,12 @@ def below_minimum_response RESPONSE end + def failed_purchase_no_name_response + <<~RESPONSE + {\"correlation_id\":\"29.7337367613551\",\"transaction_status\":\"approved\",\"validation_status\":\"success\",\"transaction_type\":\"purchase\",\"transaction_id\":\"ET106024\",\"transaction_tag\":\"10049930801\",\"method\":\"3ds\",\"amount\":\"100\",\"currency\":\"USD\",\"avs\":\"4\",\"cvv2\":\"U\",\"token\":{\"token_type\":\"FDToken\",\"token_data\":{\"value\":\"1141044316391439\"}},\"card\":{\"type\":\"VISA\",\"cardholder_name\":\"Jim Smith\",\"card_number\":\"1439\",\"exp_date\":\"1124\"},\"bank_resp_code\":\"100\",\"bank_message\":\"Approved\",\"gateway_resp_code\":\"00\",\"gateway_message\":\"Transaction Normal\",\"eCommerce_flag\":\"5\",\"retrieval_ref_no\":\"230110\"} + RESPONSE + end + def failed_refund_response yamlexcep = <<~RESPONSE --- !ruby/exception:ActiveMerchant::ResponseError @@ -733,7 +928,7 @@ def failed_refund_response body_exist: true message: RESPONSE - YAML.safe_load(yamlexcep, ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) end def successful_void_response @@ -778,7 +973,7 @@ def failed_void_response body_exist: true message: RESPONSE - YAML.safe_load(yamlexcep, ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) end def failed_capture_response @@ -818,7 +1013,7 @@ def failed_capture_response body_exist: true message: RESPONSE - YAML.safe_load(yamlexcep, ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPBadRequest', 'ActiveMerchant::ResponseError']) end def invalid_token_response @@ -857,7 +1052,7 @@ def invalid_token_response body_exist: true message: RESPONSE - YAML.safe_load(yamlexcep, ['Net::HTTPUnauthorized', 'ActiveMerchant::ResponseError']) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPUnauthorized', 'ActiveMerchant::ResponseError']) end def invalid_token_response_integration @@ -882,7 +1077,7 @@ def invalid_token_response_integration body_exist: true message: RESPONSE - YAML.safe_load(yamlexcep, ['Net::HTTPUnauthorized', 'ActiveMerchant::ResponseError']) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPUnauthorized', 'ActiveMerchant::ResponseError']) end def bad_credentials_response @@ -907,6 +1102,6 @@ def bad_credentials_response body_exist: true message: RESPONSE - YAML.safe_load(yamlexcep, ['Net::HTTPForbidden', 'ActiveMerchant::ResponseError']) + YAML.safe_load(yamlexcep, permitted_classes: ['Net::HTTPForbidden', 'ActiveMerchant::ResponseError']) end end diff --git a/test/unit/gateways/payflow_test.rb b/test/unit/gateways/payflow_test.rb index c87aedd255a..69fc901992f 100644 --- a/test/unit/gateways/payflow_test.rb +++ b/test/unit/gateways/payflow_test.rb @@ -465,9 +465,12 @@ def test_store_returns_error def test_initial_recurring_transaction_missing_parameters assert_raises ArgumentError do assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(@amount, @credit_card, + @gateway.recurring( + @amount, + @credit_card, periodicity: :monthly, - initial_transaction: {}) + initial_transaction: {} + ) end end end @@ -475,9 +478,12 @@ def test_initial_recurring_transaction_missing_parameters def test_initial_purchase_missing_amount assert_raises ArgumentError do assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do - @gateway.recurring(@amount, @credit_card, + @gateway.recurring( + @amount, + @credit_card, periodicity: :monthly, - initial_transaction: { amount: :purchase }) + initial_transaction: { amount: :purchase } + ) end end end @@ -1144,7 +1150,7 @@ def xpath_prefix_for_transaction_type(tx_type) def threeds_xpath_for_extdata(attr_name, tx_type: 'Authorization') xpath_prefix = xpath_prefix_for_transaction_type(tx_type) - %(string(#{xpath_prefix}/PayData/ExtData[@Name='#{attr_name}']/@Value)") + %(string(#{xpath_prefix}/PayData/ExtData[@Name='#{attr_name}']/@Value)) end def authorize_buyer_auth_result_path diff --git a/test/unit/gateways/paymentez_test.rb b/test/unit/gateways/paymentez_test.rb index b8b1924d096..07223ab61d9 100644 --- a/test/unit/gateways/paymentez_test.rb +++ b/test/unit/gateways/paymentez_test.rb @@ -6,13 +6,15 @@ class PaymentezTest < Test::Unit::TestCase def setup @gateway = PaymentezGateway.new(application_code: 'foo', app_key: 'bar') @credit_card = credit_card - @elo_credit_card = credit_card('6362970000457013', + @elo_credit_card = credit_card( + '6362970000457013', month: 10, year: 2020, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'elo') + brand: 'elo' + ) @amount = 100 @options = { @@ -341,6 +343,18 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_successful_inquire_with_transaction_id + response = stub_comms(@gateway, :ssl_get) do + @gateway.inquire('CI-635') + end.check_request do |method, _endpoint, _data, _headers| + assert_match('https://ccapi-stg.paymentez.com/v2/transaction/CI-635', method) + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'CI-635', response.authorization + assert response.test? + end + private def pre_scrubbed diff --git a/test/unit/gateways/paypal/paypal_common_api_test.rb b/test/unit/gateways/paypal/paypal_common_api_test.rb index da1592285f4..ebf2a530be4 100644 --- a/test/unit/gateways/paypal/paypal_common_api_test.rb +++ b/test/unit/gateways/paypal/paypal_common_api_test.rb @@ -190,10 +190,14 @@ def test_build_reference_transaction_request end def test_build_reference_transaction_gets_ip - request = REXML::Document.new(@gateway.send(:build_reference_transaction_request, - 100, - reference_id: 'id', - ip: '127.0.0.1')) + request = REXML::Document.new( + @gateway.send( + :build_reference_transaction_request, + 100, + reference_id: 'id', + ip: '127.0.0.1' + ) + ) assert_equal '100', REXML::XPath.first(request, '//n2:PaymentDetails/n2:OrderTotal').text assert_equal 'id', REXML::XPath.first(request, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:ReferenceID').text assert_equal '127.0.0.1', REXML::XPath.first(request, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:IPAddress').text diff --git a/test/unit/gateways/paypal_digital_goods_test.rb b/test/unit/gateways/paypal_digital_goods_test.rb index 605fcadb518..886253a6673 100644 --- a/test/unit/gateways/paypal_digital_goods_test.rb +++ b/test/unit/gateways/paypal_digital_goods_test.rb @@ -34,60 +34,78 @@ def test_test_redirect_url def test_setup_request_invalid_requests assert_raise ArgumentError do - @gateway.setup_purchase(100, + @gateway.setup_purchase( + 100, ip: '127.0.0.1', description: 'Test Title', return_url: 'http://return.url', - cancel_return_url: 'http://cancel.url') + cancel_return_url: 'http://cancel.url' + ) end assert_raise ArgumentError do - @gateway.setup_purchase(100, + @gateway.setup_purchase( + 100, ip: '127.0.0.1', description: 'Test Title', return_url: 'http://return.url', cancel_return_url: 'http://cancel.url', - items: []) + items: [] + ) end assert_raise ArgumentError do - @gateway.setup_purchase(100, + @gateway.setup_purchase( + 100, ip: '127.0.0.1', description: 'Test Title', return_url: 'http://return.url', cancel_return_url: 'http://cancel.url', - items: [Hash.new]) + items: [Hash.new] + ) end assert_raise ArgumentError do - @gateway.setup_purchase(100, + @gateway.setup_purchase( + 100, ip: '127.0.0.1', description: 'Test Title', return_url: 'http://return.url', cancel_return_url: 'http://cancel.url', - items: [{ name: 'Charge', - number: '1', - quantity: '1', - amount: 100, - description: 'Description', - category: 'Physical' }]) + items: [ + { + name: 'Charge', + number: '1', + quantity: '1', + amount: 100, + description: 'Description', + category: 'Physical' + } + ] + ) end end def test_build_setup_request_valid @gateway.expects(:ssl_post).returns(successful_setup_response) - @gateway.setup_purchase(100, + @gateway.setup_purchase( + 100, ip: '127.0.0.1', description: 'Test Title', return_url: 'http://return.url', cancel_return_url: 'http://cancel.url', - items: [{ name: 'Charge', - number: '1', - quantity: '1', - amount: 100, - description: 'Description', - category: 'Digital' }]) + items: [ + { + name: 'Charge', + number: '1', + quantity: '1', + amount: 100, + description: 'Description', + category: 'Digital' + } + ] + ) end private diff --git a/test/unit/gateways/paypal_express_test.rb b/test/unit/gateways/paypal_express_test.rb index 4cb2e22b47e..5be4717bfed 100644 --- a/test/unit/gateways/paypal_express_test.rb +++ b/test/unit/gateways/paypal_express_test.rb @@ -88,6 +88,7 @@ def test_get_express_details assert_equal 'FWRVKNRRZ3WUC', response.payer_id assert_equal 'buyer@jadedpallet.com', response.email assert_equal 'This is a test note', response.note + assert_equal 'PaymentActionNotInitiated', response.checkout_status assert address = response.address assert_equal 'Fred Brooks', address['name'] @@ -242,22 +243,28 @@ def test_does_not_include_flatrate_shipping_options_if_not_specified end def test_flatrate_shipping_options_are_included_if_specified_in_build_setup_request - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 0, - { - currency: 'AUD', - shipping_options: [ - { - default: true, - name: 'first one', - amount: 1000 - }, - { - default: false, - name: 'second one', - amount: 2000 - } - ] - })) + xml = REXML::Document.new( + @gateway.send( + :build_setup_request, + 'SetExpressCheckout', + 0, + { + currency: 'AUD', + shipping_options: [ + { + default: true, + name: 'first one', + amount: 1000 + }, + { + default: false, + name: 'second one', + amount: 2000 + } + ] + } + ) + ) assert_equal 'true', REXML::XPath.first(xml, '//n2:FlatRateShippingOptions/n2:ShippingOptionIsDefault').text assert_equal 'first one', REXML::XPath.first(xml, '//n2:FlatRateShippingOptions/n2:ShippingOptionName').text @@ -271,18 +278,24 @@ def test_flatrate_shipping_options_are_included_if_specified_in_build_setup_requ end def test_address_is_included_if_specified - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'Sale', 0, - { - currency: 'GBP', - address: { - name: 'John Doe', - address1: '123 somewhere', - city: 'Townville', - country: 'Canada', - zip: 'k1l4p2', - phone: '1231231231' + xml = REXML::Document.new( + @gateway.send( + :build_setup_request, + 'Sale', + 0, + { + currency: 'GBP', + address: { + name: 'John Doe', + address1: '123 somewhere', + city: 'Townville', + country: 'Canada', + zip: 'k1l4p2', + phone: '1231231231' + } } - })) + ) + ) assert_equal 'John Doe', REXML::XPath.first(xml, '//n2:PaymentDetails/n2:ShipToAddress/n2:Name').text assert_equal '123 somewhere', REXML::XPath.first(xml, '//n2:PaymentDetails/n2:ShipToAddress/n2:Street1').text @@ -311,30 +324,36 @@ def test_removes_fractional_amounts_with_twd_currency end def test_fractional_discounts_are_correctly_calculated_with_jpy_currency - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 14250, - { - items: [ - { - name: 'item one', - description: 'description', - amount: 15000, - number: 1, - quantity: 1 - }, - { - name: 'Discount', - description: 'Discount', - amount: -750, - number: 2, - quantity: 1 - } - ], - subtotal: 14250, - currency: 'JPY', - shipping: 0, - handling: 0, - tax: 0 - })) + xml = REXML::Document.new( + @gateway.send( + :build_setup_request, + 'SetExpressCheckout', + 14250, + { + items: [ + { + name: 'item one', + description: 'description', + amount: 15000, + number: 1, + quantity: 1 + }, + { + name: 'Discount', + description: 'Discount', + amount: -750, + number: 2, + quantity: 1 + } + ], + subtotal: 14250, + currency: 'JPY', + shipping: 0, + handling: 0, + tax: 0 + } + ) + ) assert_equal '142', REXML::XPath.first(xml, '//n2:OrderTotal').text assert_equal '142', REXML::XPath.first(xml, '//n2:ItemTotal').text @@ -344,30 +363,36 @@ def test_fractional_discounts_are_correctly_calculated_with_jpy_currency end def test_non_fractional_discounts_are_correctly_calculated_with_jpy_currency - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 14300, - { - items: [ - { - name: 'item one', - description: 'description', - amount: 15000, - number: 1, - quantity: 1 - }, - { - name: 'Discount', - description: 'Discount', - amount: -700, - number: 2, - quantity: 1 - } - ], - subtotal: 14300, - currency: 'JPY', - shipping: 0, - handling: 0, - tax: 0 - })) + xml = REXML::Document.new( + @gateway.send( + :build_setup_request, + 'SetExpressCheckout', + 14300, + { + items: [ + { + name: 'item one', + description: 'description', + amount: 15000, + number: 1, + quantity: 1 + }, + { + name: 'Discount', + description: 'Discount', + amount: -700, + number: 2, + quantity: 1 + } + ], + subtotal: 14300, + currency: 'JPY', + shipping: 0, + handling: 0, + tax: 0 + } + ) + ) assert_equal '143', REXML::XPath.first(xml, '//n2:OrderTotal').text assert_equal '143', REXML::XPath.first(xml, '//n2:ItemTotal').text @@ -377,30 +402,36 @@ def test_non_fractional_discounts_are_correctly_calculated_with_jpy_currency end def test_fractional_discounts_are_correctly_calculated_with_usd_currency - xml = REXML::Document.new(@gateway.send(:build_setup_request, 'SetExpressCheckout', 14250, - { - items: [ - { - name: 'item one', - description: 'description', - amount: 15000, - number: 1, - quantity: 1 - }, - { - name: 'Discount', - description: 'Discount', - amount: -750, - number: 2, - quantity: 1 - } - ], - subtotal: 14250, - currency: 'USD', - shipping: 0, - handling: 0, - tax: 0 - })) + xml = REXML::Document.new( + @gateway.send( + :build_setup_request, + 'SetExpressCheckout', + 14250, + { + items: [ + { + name: 'item one', + description: 'description', + amount: 15000, + number: 1, + quantity: 1 + }, + { + name: 'Discount', + description: 'Discount', + amount: -750, + number: 2, + quantity: 1 + } + ], + subtotal: 14250, + currency: 'USD', + shipping: 0, + handling: 0, + tax: 0 + } + ) + ) assert_equal '142.50', REXML::XPath.first(xml, '//n2:OrderTotal').text assert_equal '142.50', REXML::XPath.first(xml, '//n2:ItemTotal').text @@ -453,25 +484,31 @@ def test_button_source end def test_items_are_included_if_specified_in_build_sale_or_authorization_request - xml = REXML::Document.new(@gateway.send(:build_sale_or_authorization_request, 'Sale', 100, - { - items: [ - { - name: 'item one', - description: 'item one description', - amount: 10000, - number: 1, - quantity: 3 - }, - { - name: 'item two', - description: 'item two description', - amount: 20000, - number: 2, - quantity: 4 - } - ] - })) + xml = REXML::Document.new( + @gateway.send( + :build_sale_or_authorization_request, + 'Sale', + 100, + { + items: [ + { + name: 'item one', + description: 'item one description', + amount: 10000, + number: 1, + quantity: 3 + }, + { + name: 'item two', + description: 'item two description', + amount: 20000, + number: 2, + quantity: 4 + } + ] + } + ) + ) assert_equal 'item one', REXML::XPath.first(xml, '//n2:PaymentDetails/n2:PaymentDetailsItem/n2:Name').text assert_equal 'item one description', REXML::XPath.first(xml, '//n2:PaymentDetails/n2:PaymentDetailsItem/n2:Description').text @@ -547,15 +584,21 @@ def test_agreement_details_failure def test_build_reference_transaction_test PaypalExpressGateway.application_id = 'ActiveMerchant_FOO' - xml = REXML::Document.new(@gateway.send(:build_reference_transaction_request, 'Sale', 2000, - { - reference_id: 'ref_id', - payment_type: 'Any', - invoice_id: 'invoice_id', - description: 'Description', - ip: '127.0.0.1', - merchant_session_id: 'example_merchant_session_id' - })) + xml = REXML::Document.new( + @gateway.send( + :build_reference_transaction_request, + 'Sale', + 2000, + { + reference_id: 'ref_id', + payment_type: 'Any', + invoice_id: 'invoice_id', + description: 'Description', + ip: '127.0.0.1', + merchant_session_id: 'example_merchant_session_id' + } + ) + ) assert_equal '124', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:Version').text assert_equal 'ref_id', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:ReferenceID').text @@ -571,14 +614,20 @@ def test_build_reference_transaction_test def test_build_reference_transaction_without_merchant_session_test PaypalExpressGateway.application_id = 'ActiveMerchant_FOO' - xml = REXML::Document.new(@gateway.send(:build_reference_transaction_request, 'Sale', 2000, - { - reference_id: 'ref_id', - payment_type: 'Any', - invoice_id: 'invoice_id', - description: 'Description', - ip: '127.0.0.1' - })) + xml = REXML::Document.new( + @gateway.send( + :build_reference_transaction_request, + 'Sale', + 2000, + { + reference_id: 'ref_id', + payment_type: 'Any', + invoice_id: 'invoice_id', + description: 'Description', + ip: '127.0.0.1' + } + ) + ) assert_equal '124', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:Version').text assert_equal 'ref_id', REXML::XPath.first(xml, '//DoReferenceTransactionReq/DoReferenceTransactionRequest/n2:DoReferenceTransactionRequestDetails/n2:ReferenceID').text @@ -632,26 +681,20 @@ def test_reference_transaction_requires_fields def test_error_code_for_single_error @gateway.expects(:ssl_post).returns(response_with_error) - response = @gateway.setup_authorization(100, - return_url: 'http://example.com', - cancel_return_url: 'http://example.com') + response = @gateway.setup_authorization(100, return_url: 'http://example.com', cancel_return_url: 'http://example.com') assert_equal '10736', response.params['error_codes'] end def test_ensure_only_unique_error_codes @gateway.expects(:ssl_post).returns(response_with_duplicate_errors) - response = @gateway.setup_authorization(100, - return_url: 'http://example.com', - cancel_return_url: 'http://example.com') + response = @gateway.setup_authorization(100, return_url: 'http://example.com', cancel_return_url: 'http://example.com') assert_equal '10736', response.params['error_codes'] end def test_error_codes_for_multiple_errors @gateway.expects(:ssl_post).returns(response_with_errors) - response = @gateway.setup_authorization(100, - return_url: 'http://example.com', - cancel_return_url: 'http://example.com') + response = @gateway.setup_authorization(100, return_url: 'http://example.com', cancel_return_url: 'http://example.com') assert_equal %w[10736 10002], response.params['error_codes'].split(',') end @@ -772,6 +815,16 @@ def test_structure_correct assert_equal [], schema.validate(sub_doc) end + def test_build_reference_transaction_sets_idempotency_key + request = REXML::Document.new(@gateway.send(:build_reference_transaction_request, 'Authorization', 100, idempotency_key: 'idempotency_key')) + assert_equal 'idempotency_key', REXML::XPath.first(request, '//n2:DoReferenceTransactionRequestDetails/n2:MsgSubID').text + end + + def test_build_sale_or_authorization_request_sets_idempotency_key + request = REXML::Document.new(@gateway.send(:build_sale_or_authorization_request, 'Authorization', 100, idempotency_key: 'idempotency_key')) + assert_equal 'idempotency_key', REXML::XPath.first(request, '//n2:DoExpressCheckoutPaymentRequestDetails/n2:MsgSubID').text + end + private def successful_create_billing_agreement_response diff --git a/test/unit/gateways/paypal_test.rb b/test/unit/gateways/paypal_test.rb index a7165ef3b42..db9f5c760a0 100644 --- a/test/unit/gateways/paypal_test.rb +++ b/test/unit/gateways/paypal_test.rb @@ -260,21 +260,29 @@ def test_button_source_via_credentials_with_no_application_id end def test_item_total_shipping_handling_and_tax_not_included_unless_all_are_present - xml = @gateway.send(:build_sale_or_authorization_request, 'Authorization', @amount, @credit_card, + xml = @gateway.send( + :build_sale_or_authorization_request, + 'Authorization', @amount, @credit_card, tax: @amount, shipping: @amount, - handling: @amount) + handling: @amount + ) doc = REXML::Document.new(xml) assert_nil REXML::XPath.first(doc, '//n2:PaymentDetails/n2:TaxTotal') end def test_item_total_shipping_handling_and_tax - xml = @gateway.send(:build_sale_or_authorization_request, 'Authorization', @amount, @credit_card, + xml = @gateway.send( + :build_sale_or_authorization_request, + 'Authorization', + @amount, + @credit_card, tax: @amount, shipping: @amount, handling: @amount, - subtotal: 200) + subtotal: 200 + ) doc = REXML::Document.new(xml) assert_equal '1.00', REXML::XPath.first(doc, '//n2:PaymentDetails/n2:TaxTotal').text diff --git a/test/unit/gateways/paysafe_test.rb b/test/unit/gateways/paysafe_test.rb index 2af956edf27..2d7c73d90ec 100644 --- a/test/unit/gateways/paysafe_test.rb +++ b/test/unit/gateways/paysafe_test.rb @@ -115,6 +115,17 @@ def test_successful_purchase_with_stored_credentials assert_success response end + def test_successful_purchase_with_funding_transaction + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ funding_transaction: 'SDW_WALLET_TRANSFER' })) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r("fundingTransaction":{"type":"SDW_WALLET_TRANSFER"}), data) + assert_match(%r("profile":{.+"merchantCustomerId"), data) + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_failed_purchase @gateway.expects(:ssl_request).returns(failed_purchase_response) @@ -232,6 +243,47 @@ def test_successful_store assert_success response end + def test_merchant_ref_num_and_order_id + options = @options.merge({ order_id: '12345678' }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"merchantRefNum":"12345678"/, data) + end.respond_with(successful_purchase_response) + + assert_success response + + options = @options.merge({ order_id: '12345678', merchant_ref_num: '87654321' }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"merchantRefNum":"87654321"/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_truncate_long_address_fields + options = { + billing_address: { + address1: "This is an extremely long address, it is unreasonably long and we can't allow it.", + address2: "This is an extremely long address2, it is unreasonably long and we can't allow it.", + city: 'Lake Chargoggagoggmanchauggagoggchaubunagungamaugg', + state: 'NC', + zip: '27701' + } + } + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"street":"This is an extremely long address, it is unreasona"/, data) + assert_match(/"street2":"This is an extremely long address2, it is unreason"/, data) + assert_match(/"city":"Lake Chargoggagoggmanchauggagoggchaubuna"/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed diff --git a/test/unit/gateways/payu_latam_test.rb b/test/unit/gateways/payu_latam_test.rb index 8edfebd7bb6..664db4e5490 100644 --- a/test/unit/gateways/payu_latam_test.rb +++ b/test/unit/gateways/payu_latam_test.rb @@ -84,6 +84,7 @@ def test_failed_purchase_correct_message_when_payment_network_response_error_pre response = @gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'CONTACT_THE_ENTITY | Contactar con entidad emisora', response.message + assert_equal '290', response.error_code assert_equal 'Contactar con entidad emisora', response.params['transactionResponse']['paymentNetworkResponseErrorMessage'] @gateway.expects(:ssl_post).returns(failed_purchase_response_when_payment_network_response_error_not_expected) @@ -91,6 +92,7 @@ def test_failed_purchase_correct_message_when_payment_network_response_error_pre response = @gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'CONTACT_THE_ENTITY', response.message + assert_equal '51', response.error_code assert_nil response.params['transactionResponse']['paymentNetworkResponseErrorMessage'] end @@ -469,6 +471,16 @@ def test_mexico_required_fields end.respond_with(successful_purchase_response) end + def test_extra_parameters_fields + stub_comms(@gateway) do + @gateway.purchase(@amount, @credit_card, @options.merge({ extra_1: '123456', extra_2: 'abcdef', extra_3: 'testing' })) + end.check_request do |_endpoint, data, _headers| + assert_match(/\"EXTRA1\":\"123456\"/, data) + assert_match(/\"EXTRA2\":\"abcdef\"/, data) + assert_match(/\"EXTRA3\":\"testing\"/, data) + end.respond_with(successful_purchase_response) + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed @@ -656,7 +668,7 @@ def failed_purchase_response_when_payment_network_response_error_not_expected "orderId": 7354347, "transactionId": "15b6cec0-9eec-4564-b6b9-c846b868203e", "state": "DECLINED", - "paymentNetworkResponseCode": null, + "paymentNetworkResponseCode": "51", "paymentNetworkResponseErrorMessage": null, "trazabilityCode": null, "authorizationCode": null, diff --git a/test/unit/gateways/payway_test.rb b/test/unit/gateways/payway_test.rb index 14ccfe87ff0..f5959f9b0fd 100644 --- a/test/unit/gateways/payway_test.rb +++ b/test/unit/gateways/payway_test.rb @@ -11,7 +11,7 @@ def setup @amount = 1000 @credit_card = ActiveMerchant::Billing::CreditCard.new( - number: 4564710000000004, + number: '4564710000000004', month: 2, year: 2019, first_name: 'Bob', diff --git a/test/unit/gateways/plexo_test.rb b/test/unit/gateways/plexo_test.rb new file mode 100644 index 00000000000..d05db0f95cb --- /dev/null +++ b/test/unit/gateways/plexo_test.rb @@ -0,0 +1,884 @@ +require 'test_helper' + +class PlexoTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = PlexoGateway.new(client_id: 'abcd', api_key: 'efgh', merchant_id: 'test090') + + @amount = 100 + @credit_card = credit_card('5555555555554444', month: '12', year: '2024', verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') + @declined_card = credit_card('5555555555554445') + @options = { + email: 'snavatta@plexo.com.uy', + ip: '127.0.0.1', + items: [ + { + name: 'prueba', + description: 'prueba desc', + quantity: '1', + price: '100', + discount: '0' + } + ], + amount_details: { + tip_amount: '5' + }, + metadata: { + custom_one: 'test1', + test_a: 'abc' + }, + identification_type: '1', + identification_value: '123456', + billing_address: address + } + + @cancel_options = { + description: 'Test desc', + reason: 'requested by client' + } + end + + def test_successful_purchase + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'You have been mocked', response.message + end + + def test_failed_purchase + @gateway.expects(:ssl_post).returns(failed_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_successful_authorize + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal 'test090', request['MerchantId'] + assert_equal @credit_card.number, request['paymentMethod']['Card']['Number'] + assert_equal @credit_card.verification_value, request['paymentMethod']['Card']['Cvc'] + assert_equal @credit_card.first_name, request['paymentMethod']['Card']['Cardholder']['FirstName'] + assert_equal @options[:email], request['paymentMethod']['Card']['Cardholder']['Email'] + assert_equal @options[:identification_type], request['paymentMethod']['Card']['Cardholder']['Identification']['Type'] + assert_equal @options[:identification_value], request['paymentMethod']['Card']['Cardholder']['Identification']['Value'] + assert_equal @options[:billing_address][:city], request['paymentMethod']['Card']['Cardholder']['BillingAddress']['City'] + assert_equal @options[:billing_address][:country], request['paymentMethod']['Card']['Cardholder']['BillingAddress']['Country'] + assert_equal @options[:billing_address][:address1], request['paymentMethod']['Card']['Cardholder']['BillingAddress']['Line1'] + assert_equal @options[:billing_address][:address2], request['paymentMethod']['Card']['Cardholder']['BillingAddress']['Line2'] + assert_equal @options[:billing_address][:zip], request['paymentMethod']['Card']['Cardholder']['BillingAddress']['PostalCode'] + assert_equal @options[:billing_address][:state], request['paymentMethod']['Card']['Cardholder']['BillingAddress']['State'] + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_successful_authorize_with_items + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + request['Items'].each_with_index do |item, index| + assert_not_nil item['ReferenceId'] + assert_equal item['Name'], @options[:items][index][:name] if item['Name'] + assert_equal item['Description'], @options[:items][index][:description] if item['Description'] + assert_equal item['Quantity'], @options[:items][index][:quantity] if item['Quantity'] + assert_equal item['Price'], @options[:items][index][:price] if item['Price'] + assert_equal item['Discount'], @options[:items][index][:discount] if item['Discount'] + end + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_successful_authorize_with_meta_fields + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + @options[:metadata].keys.each do |meta_key| + camel_key = meta_key.to_s.camelize + assert_equal request['Metadata'][camel_key], @options[:metadata][meta_key] + end + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_successful_authorize_with_finger_print + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ finger_print: 'USABJHABSFASNJKN123532' })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['BrowserDetails']['DeviceFingerprint'], 'USABJHABSFASNJKN123532' + end.respond_with(successful_authorize_response) + end + + def test_successful_reordering_of_amount_in_authorize + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + original_response = JSON.parse(successful_authorize_response) + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal response.params['amount'], original_response['amount']['total'] + assert_equal response.params['currency'], original_response['amount']['currency'] + assert_equal response.params['amount_details'], original_response['amount']['details'] + end + + def test_successful_authorize_with_extra_options + other_fields = { + installments: '1', + statement_descriptor: 'Plexo * Test', + customer_id: 'customer1', + cardholder_birthdate: '1999-08-18T19:49:37.023Z' + } + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(other_fields)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['Installments'], other_fields[:installments] + assert_equal request['CustomerId'], other_fields[:customer_id] + assert_equal request['StatementDescriptor'], other_fields[:statement_descriptor] + assert_equal request['paymentMethod']['Card']['Cardholder']['Birthdate'], other_fields[:cardholder_birthdate] + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_successful_authorize_with_amount_fields + amount_fields = { + taxed_amount: '100', + tip_amount: '32', + discount_amount: '10', + taxable_amount: '302', + tax: { + type: '17934', + amount: '22', + rate: '0.22' + } + } + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ amount_details: amount_fields, currency: 'USD' })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['Amount']['Currency'], 'USD' + assert_equal request['Amount']['Details']['TaxedAmount'], amount_fields[:taxed_amount] + assert_equal request['Amount']['Details']['TipAmount'], amount_fields[:tip_amount] + assert_equal request['Amount']['Details']['DiscountAmount'], amount_fields[:discount_amount] + assert_equal request['Amount']['Details']['TaxableAmount'], amount_fields[:taxable_amount] + assert_equal request['Amount']['Details']['Tax']['Type'], amount_fields[:tax][:type] + assert_equal request['Amount']['Details']['Tax']['Amount'], amount_fields[:tax][:amount] + assert_equal request['Amount']['Details']['Tax']['Rate'], amount_fields[:tax][:rate] + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_failed_authorize + @gateway.expects(:ssl_post).returns(failed_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_successful_capture + response = stub_comms do + @gateway.capture(@amount, '123456abcdef', { reference_id: 'reference123' }) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['ReferenceId'], 'reference123' + assert_includes endpoint, '123456abcdef' + end.respond_with(successful_capture_response) + + assert_success response + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(failed_capture_response) + + response = @gateway.capture(@amount, @credit_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_successful_refund + refund_options = { + reference_id: 'reference123', + refund_type: 'partial-refund', + description: 'my description', + reason: 'reason abc' + } + response = stub_comms do + @gateway.refund(@amount, '123456abcdef', refund_options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['ReferenceId'], refund_options[:reference_id] + assert_equal request['Type'], refund_options[:refund_type] + assert_equal request['Description'], refund_options[:description] + assert_equal request['Reason'], refund_options[:reason] + assert_includes endpoint, '123456abcdef' + end.respond_with(successful_refund_response) + + assert_success response + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + + response = @gateway.refund(@amount, @credit_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_successful_void + void_options = { + reference_id: 'reference123', + description: 'my description', + reason: 'reason abc' + } + response = stub_comms do + @gateway.void('123456abcdef', void_options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['ReferenceId'], void_options[:reference_id] + assert_equal request['Description'], void_options[:description] + assert_equal request['Reason'], void_options[:reason] + assert_includes endpoint, '123456abcdef' + end.respond_with(successful_void_response) + + assert_success response + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + + response = @gateway.void(@credit_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_successful_verify + @gateway.expects(:ssl_post).returns(successful_verify_response) + + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'You have been mocked.', response.message + end + + def test_successful_verify_with_custom_amount + stub_comms do + @gateway.verify(@credit_card, @options.merge({ verify_amount: '900' })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['Amount']['Total'], '9.00' + end.respond_with(successful_verify_response) + end + + def test_failed_verify + @gateway.expects(:ssl_post).returns(failed_verify_response) + + response = @gateway.verify(@credit_card, @options) + assert_failure response + assert_equal 400, response.error_code + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def pre_scrubbed + <<~PRE_SCRUBBED + opening connection to api.testing.plexo.com.uy:443... + opened + starting SSL for api.testing.plexo.com.uy:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- "POST /v1/payments/628b723aa450dab85ba2fa03/captures HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Basic MjIxOjlkZWZhZWVlYmMzOTQ1NDFhZmY2MzMyOTE4MmRkODQyNDA1MTJhYTI0NWE0NDY2MDkxZWQ3MGY2OTAxYjQ5NDc=\r\nX-Mock-Tokenization: true\r\nX-Mock-Switcher: true\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: api.testing.plexo.com.uy\r\nContent-Length: 66\r\n\r\n" + <- "{\"ReferenceId\":\"e6742109bb60458b1c5a7c69ffcc3f54\",\"Amount\":\"1.00\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Mon, 23 May 2022 11:38:35 GMT\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "X-MiniProfiler-Ids: [\"c6b2ce60-757c-4115-b802-e33a27c2e311\",\"e1533461-72dc-4693-97a6-deea47601ca4\",\"da8b919f-a1f8-4051-870d-3679f4c8ac6b\",\"4465311a-ab60-470d-8f69-e06eed35c271\",\"c4b23b7d-e824-4fd6-95b9-82fa4786f4a2\",\"c5fe47c7-6155-4eb7-b9f4-84cd7fae7acf\",\"80e2f132-1ac1-4b25-b030-5eaccd44a0db\",\"525c97a7-5df7-4dd5-b1da-4c6abe9a5995\",\"98694fd6-f3ff-497a-b6d4-477a50a093aa\",\"802b9242-97c6-4438-bd72-960dbdf2f752\",\"7aa9078c-12f1-41f4-bc57-c77fb8a9ecc8\",\"4890d7e1-22c9-4e9d-afe1-88149e743aa0\",\"cafed17f-08ce-49cc-91d0-d8d9865facc7\",\"98fea53d-ad00-44cb-8e82-0829e5c8aaee\",\"5730d4fa-1c70-4679-a097-d9c8b7156f2d\",\"ba7d9c5a-e2bc-461f-b87d-552ae9fabb65\",\"3b1dbbbe-8112-4293-9be3-c865741c5494\",\"3ab01bd5-a2b5-4d9c-84c7-9c743f1e9978\",\"d6e397a3-cf95-413c-b3c6-4729aa463d33\",\"fc9cb79e-ab22-42b0-b611-0b3f62a203bb\",\"b16fd902-f50a-43e2-8e82-cc0fe763b16b\",\"dc702114-866c-4b9a-bc07-291b0b0f8b73\"]\r\n" + -> "x-correlation-id: 24ebd1ee-a69a-4163-85cf-e5a1ab7fd26b\r\n" + -> "Strict-Transport-Security: max-age=15724800; includeSubDomains\r\n" + -> "\r\n" + -> "192\r\n" + reading 402 bytes... + -> "{\"id\":\"628b723ba450dab85ba2fa0a\",\"uniqueId\":\"978260656060936192\",\"parentId\":\"cf8ecc4a-b0ed-4a40-945e-0eaff39e66f9\",\"referenceId\":\"e6742109bb60458b1c5a7c69ffcc3f54\",\"type\":\"capture\",\"status\":\"approved\",\"createdAt\":\"2022-05-23T11:38:35.6091676Z\",\"processedAt\":\"2022-05-23T11:38:35.6091521Z\",\"resultCode\":\"0\",\"resultMessage\":\"You have been mocked.\",\"authorization\":\"12133\",\"ticket\":\"111111\",\"amount\":1.00}" + read 402 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<~POST_SCRUBBED + opening connection to api.testing.plexo.com.uy:443... + opened + starting SSL for api.testing.plexo.com.uy:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- "POST /v1/payments/628b723aa450dab85ba2fa03/captures HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Basic [FILTERED]=\r\nX-Mock-Tokenization: true\r\nX-Mock-Switcher: true\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: api.testing.plexo.com.uy\r\nContent-Length: 66\r\n\r\n" + <- "{\"ReferenceId\":\"e6742109bb60458b1c5a7c69ffcc3f54",\"Amount\":\"1.00\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Mon, 23 May 2022 11:38:35 GMT\r\n" + -> "Content-Type: application/json; charset=utf-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "X-MiniProfiler-Ids: [\"c6b2ce60-757c-4115-b802-e33a27c2e311\",\"e1533461-72dc-4693-97a6-deea47601ca4\",\"da8b919f-a1f8-4051-870d-3679f4c8ac6b\",\"4465311a-ab60-470d-8f69-e06eed35c271\",\"c4b23b7d-e824-4fd6-95b9-82fa4786f4a2\",\"c5fe47c7-6155-4eb7-b9f4-84cd7fae7acf\",\"80e2f132-1ac1-4b25-b030-5eaccd44a0db\",\"525c97a7-5df7-4dd5-b1da-4c6abe9a5995\",\"98694fd6-f3ff-497a-b6d4-477a50a093aa\",\"802b9242-97c6-4438-bd72-960dbdf2f752\",\"7aa9078c-12f1-41f4-bc57-c77fb8a9ecc8\",\"4890d7e1-22c9-4e9d-afe1-88149e743aa0\",\"cafed17f-08ce-49cc-91d0-d8d9865facc7\",\"98fea53d-ad00-44cb-8e82-0829e5c8aaee\",\"5730d4fa-1c70-4679-a097-d9c8b7156f2d\",\"ba7d9c5a-e2bc-461f-b87d-552ae9fabb65\",\"3b1dbbbe-8112-4293-9be3-c865741c5494\",\"3ab01bd5-a2b5-4d9c-84c7-9c743f1e9978\",\"d6e397a3-cf95-413c-b3c6-4729aa463d33\",\"fc9cb79e-ab22-42b0-b611-0b3f62a203bb\",\"b16fd902-f50a-43e2-8e82-cc0fe763b16b\",\"dc702114-866c-4b9a-bc07-291b0b0f8b73\"]\r\n" + -> "x-correlation-id: 24ebd1ee-a69a-4163-85cf-e5a1ab7fd26b\r\n" + -> "Strict-Transport-Security: max-age=15724800; includeSubDomains\r\n" + -> "\r\n" + -> "192\r\n" + reading 402 bytes... + -> "{\"id\":\"628b723ba450dab85ba2fa0a\",\"uniqueId\":\"978260656060936192\",\"parentId\":\"cf8ecc4a-b0ed-4a40-945e-0eaff39e66f9\",\"referenceId\":\"e6742109bb60458b1c5a7c69ffcc3f54",\"type\":\"capture\",\"status\":\"approved\",\"createdAt\":\"2022-05-23T11:38:35.6091676Z\",\"processedAt\":\"2022-05-23T11:38:35.6091521Z\",\"resultCode\":\"0\",\"resultMessage\":\"You have been mocked.\",\"authorization\":\"12133\",\"ticket\":\"111111\",\"amount\":1.00}" + read 402 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + POST_SCRUBBED + end + + def failed_purchase_response + <<~RESPONSE + { + "code": "merchant-not-found", + "message": "The requested Merchant was not found.", + "type": "invalid-request-error", + "status": 400 + } + RESPONSE + end + + def successful_authorize_response + <<~RESPONSE + { + "id": "62878b1fa450dab85ba2f983", + "token": "7c23b951-599f-462e-8a47-6bbbb4dc5ad0", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "status": "approved", + "processingMethod": "api", + "browserDetails": { + "DeviceFingerprint": "12345", + "IpAddress": "127.0.0.1" + }, + "createdAt": "2022-05-20T12:35:43.1389809Z", + "merchant": { + "id": 3243, + "name": "spreedly", + "settings": { + "id": 41363, + "issuerId": 4, + "issuer": { + "id": 4, + "code": "mastercard", + "name": "MASTERCARD", + "type": "online" + }, + "metadata": { + "ProviderCommerceNumber": "153289", + "TerminalNumber": "1K153289", + "SoftDescriptor": "VTEX-Testing", + "PaymentProcessorId": "oca" + }, + "paymentProcessor": { + "acquirer": "oca", + "settings": { + "commerce": { + "fields": [ + { + "name": "ProviderCommerceNumber", + "type": 2049 + }, + { + "name": "TerminalNumber", + "type": 2051 + } + ] + }, + "fingerprint": { + "name": "cybersource-oca" + }, + "fields": [ + { + "name": "Email", + "type": 261 + }, + { + "name": "FirstName", + "type": 271 + }, + { + "name": "LastName", + "type": 272 + }, + { + "name": "CVC", + "type": 33154 + } + ] + } + } + }, + "clientId": 221 + }, + "client": { + "id": 221, + "name": "Spreedly", + "tier": 2, + "sessionTimeInSeconds": 36000 + }, + "paymentMethod": { + "type": "card", + "card": { + "name": "555555XXXXXX4444", + "bin": "555555", + "last4": "4444", + "expMonth": 12, + "expYear": 24, + "cardholder": { + "firstName": "Santiago", + "lastName": "Navatta", + "email": "snavatta@plexo.com.uy" + }, + "fingerprint": "2cccefc7e6e54644b5f5540aaab7744b" + }, + "issuer": { + "id": "mastercard", + "name": "MasterCard", + "pictureUrl": "https://static.plexo.com.uy/issuers/4.svg", + "type": "online" + }, + "processor": { + "acquirer": "oca" + } + }, + "installments": 1, + "amount": { + "currency": "UYU", + "total": 147, + "details": { + "taxedAmount": 0 + } + }, + "items": [ + { + "referenceId": "7c34953392e84949ab511667db0ebef2", + "name": "prueba", + "description": "prueba desc", + "quantity": 1, + "price": 100, + "discount": 0 + } + ], + "capture": { + "method": "manual" + }, + "transactions": [ + { + "id": "62878b1fa450dab85ba2f987", + "uniqueId": "977187868889886720", + "parentId": "7c23b951-599f-462e-8a47-6bbbb4dc5ad0", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "type": "authorization", + "status": "approved", + "createdAt": "2022-05-20T12:35:43.2161946Z", + "processedAt": "2022-05-20T12:35:43.2161798Z", + "resultCode": "0", + "resultMessage": "You have been mocked.", + "authorization": "12133", + "ticket": "111111", + "amount": 147 + } + ] + } + RESPONSE + end + + def successful_purchase_response + <<~RESPONSE + { + "id": "6305dd2d000d6ed5d1ecf79b", + "token": "82ae122c-d235-43bc-a454-fba16b2ae3a4", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "status": "approved", + "processingMethod": "api", + "createdAt": "2022-08-24T08:11:25.677Z", + "updatedAt": "2022-08-24T08:11:26.2893146Z", + "processedAt": "2022-08-24T08:11:26.2893146Z", + "merchant": { + "id": 3243, + "name": "spreedly", + "settings": { + "merchantIdentificationNumber": "98001456", + "paymentProcessor": { + "acquirer": "fiserv" + } + }, + "clientId": 221 + }, + "client": { + "id": 221, + "name": "Spreedly", + "tier": 2, + "sessionTimeInSeconds": 36000 + }, + "paymentMethod": { + "id": "mastercard", + "name": "MASTERCARD", + "type": "card", + "card": { + "name": "555555XXXXXX4444", + "bin": "555555", + "last4": "4444", + "expMonth": 12, + "expYear": 24, + "cardholder": { + "firstName": "Santiago", + "lastName": "Navatta", + "email": "snavatta@plexo.com.uy", + "identification": { + "type": 1, + "value": "123456" + }, + "billingAddress": { + "city": "Karachi", + "country": "Pakistan", + "line1": "street 4" + } + }, + "type": "credit", + "origin": "international", + "token": "03d43b25971546e0ab27e8b4698c9b7d" + }, + "issuer": { + "id": "mastercard", + "name": "MasterCard", + "pictureUrl": "https://static.plexo.com.uy/issuers/4.svg", + "type": "online" + }, + "processor": { + "id": 4, + "acquirer": "fiserv" + } + }, + "installments": 1, + "amount": { + "currency": "UYU", + "total": 147.0, + "details": { + "tax": { + "type": "17934", + "amount": 22.0 + }, + "taxedAmount": 100.0, + "tipAmount": 25.0, + "discountAmount": 0.0 + } + }, + "items": [ + { + "referenceId": "7c34953392e84949ab511667db0ebef2", + "name": "prueba", + "description": "prueba desc", + "quantity": 1, + "price": 100.0, + "discount": 0.0 + } + ], + "transactions": [ + { + "id": "6305dd2e000d6ed5d1ecf79f", + "uniqueId": "1011910592648278016", + "parentId": "82ae122c-d235-43bc-a454-fba16b2ae3a4", + "traceId": "cbf814cd-8b28-4145-ac0b-7381980015e8", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "type": "purchase", + "status": "approved", + "createdAt": "2022-08-24T08:11:26.2893133Z", + "processedAt": "2022-08-24T08:11:26.2893129Z", + "resultCode": "0", + "resultMessage": "You have been mocked", + "authorization": "1234567890", + "ticket": "1234567890", + "amount": 147.0 + } + ] + } + RESPONSE + end + + def failed_authorize_response + <<~RESPONSE + { + "code": "merchant-not-found", + "message": "The requested Merchant was not found.", + "type": "invalid-request-error", + "status": 400 + } + RESPONSE + end + + def successful_capture_response + <<~RESPONSE + { + "id": "62878b1fa450dab85ba2f987", + "uniqueId": "977187868889886720", + "parentId": "7c23b951-599f-462e-8a47-6bbbb4dc5ad0", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "type": "capture", + "status": "approved", + "createdAt": "2022-05-20T12:35:43.216Z", + "processedAt": "2022-05-20T12:35:43.216Z", + "resultCode": "0", + "resultMessage": "You have been mocked.", + "authorization": "12133", + "ticket": "111111", + "amount": 147 + } + RESPONSE + end + + def failed_capture_response + <<~RESPONSE + { + "code": "internal-error", + "message": "An internal error occurred. Contact support.", + "type": "api-error", + "status": 400 + } + RESPONSE + end + + def successful_refund_response + <<~RESPONSE + { + "id": "62878b1fa450dab85ba2f987", + "uniqueId": "977187868889886720", + "parentId": "7c23b951-599f-462e-8a47-6bbbb4dc5ad0", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "type": "refund", + "status": "approved", + "resultCode": "0", + "resultMessage": "You have been mocked.", + "authorization": "12133", + "ticket": "111111", + "amount": 147, + "reason": "ClientRequest" + } + RESPONSE + end + + def failed_refund_response + <<~RESPONSE + { + "code": "internal-error", + "message": "An internal error occurred. Contact support.", + "type": "api-error", + "status": 400 + } + RESPONSE + end + + def successful_void_response + <<~RESPONSE + { + "id": "62878c0fa450dab85ba2f994", + "uniqueId": "977188875178913792", + "parentId": "49fe7306-d706-43e4-97cd-8de94683c9ae", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "type": "cancellation", + "status": "approved", + "createdAt": "2022-05-20T12:39:43.134Z", + "processedAt": "2022-05-20T12:39:43.134Z", + "resultCode": "0", + "resultMessage": "You have been mocked.", + "authorization": "12133", + "ticket": "111111", + "amount": 147.0 + } + RESPONSE + end + + def failed_void_response + <<~RESPONSE + { + "code": "internal-error", + "message": "An internal error occurred. Contact support.", + "type": "api-error", + "status": 400 + } + RESPONSE + end + + def successful_verify_response + <<~RESPONSE + { + "id": "62ac2c5eaf353be57867f977", + "token": "7220c5cc-4b57-43e6-ae91-3fd3f3e8d49f", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "status": "approved", + "processingMethod": "api", + "browserDetails": { + "DeviceFingerprint": "12345", + "IpAddress": "127.0.0.1" + }, + "createdAt": "2022-06-17T07:25:18.1421498Z", + "merchant": { + "id": 3243, + "name": "spreedly", + "settings": { + "id": 41363, + "issuerId": 4, + "issuer": { + "id": 4, + "code": "mastercard", + "name": "MASTERCARD", + "type": "online" + }, + "metadata": { + "ProviderCommerceNumber": "153289", + "TerminalNumber": "1K153289", + "SoftDescriptor": "VTEX-Testing", + "PaymentProcessorId": "oca" + }, + "paymentProcessor": { + "acquirer": "oca", + "settings": { + "commerce": { + "fields": [ + { + "name": "ProviderCommerceNumber", + "type": 2049 + }, + { + "name": "TerminalNumber", + "type": 2051 + } + ] + }, + "fingerprint": { + "name": "cybersource-oca" + }, + "fields": [ + { + "name": "Email", + "type": 261 + }, + { + "name": "FirstName", + "type": 271 + }, + { + "name": "LastName", + "type": 272 + }, + { + "name": "CVC", + "type": 33154 + } + ] + } + } + }, + "clientId": 221 + }, + "client": { + "id": 221, + "name": "Spreedly", + "tier": 2, + "sessionTimeInSeconds": 36000 + }, + "paymentMethod": { + "type": "card", + "card": { + "name": "555555XXXXXX4444", + "bin": "555555", + "last4": "4444", + "expMonth": 12, + "expYear": 24, + "cardholder": { + "firstName": "Santiago", + "lastName": "Navatta", + "email": "snavatta@plexo.com.uy" + }, + "fingerprint": "36e2219cc4734a61af258905c1c59ba4" + }, + "issuer": { + "id": "mastercard", + "name": "MasterCard", + "pictureUrl": "https://static.plexo.com.uy/issuers/4.svg", + "type": "online" + }, + "processor": { + "acquirer": "oca" + } + }, + "installments": 1, + "amount": { + "currency": "UYU", + "total": 20 + }, + "items": [ + { + "referenceId": "997d4aafe29b4421ac52a3ddf5b28dfd", + "name": "card-verification", + "quantity": 1, + "price": 20 + } + ], + "capture": { + "method": "manual", + "delay": 0 + }, + "metadata": { + "One": "abc" + }, + "transactions": [ + { + "id": "62ac2c5eaf353be57867f97b", + "uniqueId": "987256610059481088", + "parentId": "7220c5cc-4b57-43e6-ae91-3fd3f3e8d49f", + "referenceId": "e7dbc06224f646ad8e63ec1c6e670a39", + "type": "authorization", + "status": "approved", + "createdAt": "2022-06-17T07:25:18.1796516Z", + "processedAt": "2022-06-17T07:25:18.1796366Z", + "resultCode": "0", + "resultMessage": "You have been mocked.", + "authorization": "12133", + "ticket": "111111", + "amount": 20 + } + ] + } + RESPONSE + end + + def failed_verify_response + <<~RESPONSE + { + "code": "invalid-transaction-state", + "message": "The selected payment state is not valid.", + "type": "api-error", + "status": 400 + } + RESPONSE + end +end diff --git a/test/unit/gateways/priority_test.rb b/test/unit/gateways/priority_test.rb index 1f3c6aae75e..4d53c73417a 100644 --- a/test/unit/gateways/priority_test.rb +++ b/test/unit/gateways/priority_test.rb @@ -3,7 +3,7 @@ class PriorityTest < Test::Unit::TestCase include CommStub def setup - @gateway = PriorityGateway.new(key: 'sandbox_key', secret: 'secret', merchant_id: 'merchant_id') + @gateway = PriorityGateway.new(api_key: 'sandbox_key', secret: 'secret', merchant_id: 'merchant_id') @amount = 4 @credit_card = credit_card @invalid_credit_card = credit_card('4111') @@ -247,6 +247,28 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_successful_credit + response = stub_comms do + @gateway.credit(@amount, @credit_card, @options) + end.respond_with(successful_credit_response) + + assert_success response + assert_equal @approval_message, response.message + assert_equal 'Return', response.params['type'] + assert response.test? + end + + def test_failed_credit_invalid_credit_card_month + response = stub_comms do + @gateway.credit(@amount, @invalid_credit_card, @options) + end.respond_with(failed_credit_response) + + assert_failure response + assert_equal 'ValidationError', response.error_code + assert_equal 'Year, Month, and Day parameters describe an un-representable DateTime.', response.message + assert response.test? + end + def successful_refund_response %( { @@ -1254,4 +1276,77 @@ def post_scrubbed {\"achIndicator\":null,\"amount\":2.11,\"authCode\":null,\"authOnly\":false,\"bankAccount\":null,\"cardAccount\":{\"avsStreet\":\"1\",\"avsZip\":\"88888\",\"cvv\":\"[FILTERED]\",\"entryMode\":\"Keyed\",\"expiryDate\":\"01/29\",\"expiryMonth\":\"01\",\"expiryYear\":\"29\",\"last4\":null,\"magstripe\":null,\"number\":\"[FILTERED]\"},\"cardPresent\":false,\"cardPresentType\":\"CardNotPresent\",\"isAuth\":true,\"isSettleFunds\":true,\"isTicket\":false,\"merchantId\":12345678,\"mxAdvantageEnabled\":false,\"mxAdvantageFeeLabel\":\"\",\"paymentType\":\"Sale\",\"purchases\":[{\"taxRate\":\"0.0000\",\"additionalTaxRate\":null,\"discountRate\":null}],\"shouldGetCreditCardLevel\":true,\"shouldVaultCard\":true,\"source\":\"Tester\",\"sourceZip\":\"K1C2N6\",\"taxExempt\":false,\"tenderType\":\"Card\",\"terminals\":[]} ) end + + def successful_credit_response + %( + { + "created": "2022-07-21T15:12:48.543Z", + "paymentToken": "PfUCohlRQpcR1cKarXkKFHV4pjcX2RJl", + "id": 87523059, + "creatorName": "spreedlyprapi", + "replayId": 16584146247154989, + "isDuplicate": false, + "shouldVaultCard": true, + "merchantId": 1000003310, + "batch": "0015", + "batchId": 10000000275553, + "tenderType": "Card", + "currency": "USD", + "amount": "-26.34", + "meta": "I like beer", + "cardAccount": { + "cardType": "Visa", + "entryMode": "Keyed", + "last4": "4242", + "cardId": "ESkW1RwQPcSW12HOH4wdBllGQMsf", + "token": "PfUCohlRQpcR1cKarXkKFHV4pjcX2RJl", + "expiryMonth": "09", + "expiryYear": "23", + "hasContract": false, + "cardPresent": false + }, + "posData": { + "panCaptureMethod": "Manual" + }, + "authOnly": false, + "authCode": "PPS6bf", + "status": "Approved", + "risk": { + "cvvResponseCode": "N", + "cvvResponse": "No Match", + "cvvMatch": false, + "avsResponseCode": "D", + "avsAddressMatch": true, + "avsZipMatch": true + }, + "requireSignature": false, + "settledAmount": "0", + "settledCurrency": "USD", + "cardPresent": false, + "authMessage": "Approved or completed successfully. ", + "availableAuthAmount": "0", + "reference": "220215004077", + "tax": "0", + "invoice": "12345", + "type": "Return", + "taxExempt": false, + "reviewIndicator": 1, + "source": "Spreedly", + "shouldGetCreditCardLevel": false + } + ) + end + + def failed_credit_response + %( + { + "errorCode": "ValidationError", + "message": "Validation error happened", + "details": [ + "Year, Month, and Day parameters describe an un-representable DateTime." + ], + "responseCode": "eSD7row8WL3JkUkOymB3FlQ" + } + ) + end end diff --git a/test/unit/gateways/rapyd_test.rb b/test/unit/gateways/rapyd_test.rb index 50c471e87f0..2fc153e1bcd 100644 --- a/test/unit/gateways/rapyd_test.rb +++ b/test/unit/gateways/rapyd_test.rb @@ -1,14 +1,25 @@ require 'test_helper' class RapydTest < Test::Unit::TestCase + include CommStub + def setup @gateway = RapydGateway.new(secret_key: 'secret_key', access_key: 'access_key') @credit_card = credit_card + @check = check @amount = 100 + @authorization = 'cus_9e1b5a357b2b7f25f8dd98827fbc4f22|card_cf105df9e77462deb34ffef33c3e3d05' @options = { pm_type: 'in_amex_card', - currency: 'USD' + currency: 'USD', + complete_payment_url: 'www.google.com', + error_payment_url: 'www.google.com', + description: 'Describe this transaction', + statement_descriptor: 'Statement Descriptor', + email: 'test@example.com', + billing_address: address(name: 'Jim Reynolds'), + order_id: '987654321' } @metadata = { @@ -29,32 +40,139 @@ def setup } @ewallet_id = 'ewallet_1a867a32b47158b30a8c17d42f12f3f1' + + @address_object = address(line_1: '123 State Street', line_2: 'Apt. 34', phone_number: '12125559999') end def test_successful_purchase - @gateway.expects(:ssl_request).returns(successful_purchase_response) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: address(name: 'Joe John-ston'))) + end.check_request do |_method, _endpoint, data, _headers| + assert_equal JSON.parse(data)['address']['name'], 'Joe John-ston' + end.respond_with(successful_purchase_response) - response = @gateway.purchase(@amount, @credit_card, @options) assert_success response - assert_equal 'payment_716ce0efc63aa8d91579e873d29d9d5e', response.authorization + assert_equal 'payment_716ce0efc63aa8d91579e873d29d9d5e', response.authorization.split('|')[0] + end + + def test_successful_purchase_without_cvv + @credit_card.verification_value = nil + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"number":"4242424242424242","expiration_month":"9","expiration_year":"2024","name":"Longbob Longsen/, data) + end.respond_with(successful_purchase_response) + assert_success response + assert_equal 'payment_716ce0efc63aa8d91579e873d29d9d5e', response.authorization.split('|')[0] end def test_successful_purchase_with_ach - @gateway.expects(:ssl_request).returns(successful_ach_purchase_response) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @check, @options.merge(billing_address: address(name: 'Joe John-ston'))) + end.check_request do |_method, _endpoint, data, _headers| + assert_nil JSON.parse(data)['capture'] + end.respond_with(successful_ach_purchase_response) - response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'ACT', response.params['data']['status'] end - def test_successful_purchase_with_options - @gateway.expects(:ssl_request).returns(successful_purchase_with_options_response) + def test_successful_purchase_with_token + @options[:customer_id] = 'cus_9e1b5a357b2b7f25f8dd98827fbc4f22' + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @authorization, @options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['payment_method'], @authorization.split('|')[1] + assert_equal request['customer'], @options[:customer_id] + end.respond_with(successful_purchase_with_options_response) - response = @gateway.purchase(@amount, @credit_card, @options.merge(metadata: @metadata)) assert_success response assert_equal @metadata, response.params['data']['metadata'].deep_transform_keys(&:to_sym) end + def test_successful_purchase_with_payment_options + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"complete_payment_url":"www.google.com"/, data) + assert_match(/"error_payment_url":"www.google.com"/, data) + assert_match(/"description":"Describe this transaction"/, data) + assert_match(/"statement_descriptor":"Statement Descriptor"/, data) + assert_match(/"merchant_reference_id":"987654321"/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_successful_purchase_with_explicit_merchant_reference_id + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ merchant_reference_id: '99988877776' })) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"complete_payment_url":"www.google.com"/, data) + assert_match(/"error_payment_url":"www.google.com"/, data) + assert_match(/"description":"Describe this transaction"/, data) + assert_match(/"statement_descriptor":"Statement Descriptor"/, data) + assert_match(/"merchant_reference_id":"99988877776"/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credential + @options[:stored_credential] = { + reason_type: 'recurring', + network_transaction_id: '12345' + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['payment_method']['fields']['network_reference_id'], @options[:stored_credential][:network_transaction_id] + assert_equal request['initiation_type'], @options[:stored_credential][:reason_type] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_network_transaction_id_and_initiation_type_fields + @options[:network_transaction_id] = '54321' + @options[:initiation_type] = 'customer_present' + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['payment_method']['fields']['network_reference_id'], @options[:network_transaction_id] + assert_equal request['initiation_type'], @options[:initiation_type] + end.respond_with(successful_purchase_response) + end + + def test_success_purchase_with_recurrence_type + @options[:recurrence_type] = 'recurring' + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['recurrence_type'], @options[:recurrence_type] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_3ds_gateway_specific + @options[:three_d_secure] = { + required: true, + version: '2.1.0' + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['payment_method_options']['3d_required'], true + assert_equal request['payment_method_options']['3d_version'], '2.1.0' + assert request['complete_payment_url'] + assert request['error_payment_url'] + end.respond_with(successful_purchase_response) + end + def test_failed_purchase @gateway.expects(:ssl_request).returns(failed_purchase_response) @@ -131,27 +249,139 @@ def test_failed_void end def test_successful_verify - @gateway.expects(:ssl_request).twice.returns(successful_authorize_response, successful_void_response) + @gateway.expects(:ssl_request).returns(successful_verify_response) response = @gateway.verify(@credit_card, @options) assert_success response assert_equal 'SUCCESS', response.message end - def test_successful_verify_with_failed_void - @gateway.expects(:ssl_request).twice.returns(successful_authorize_response, failed_void_response) + def test_failed_verify + @gateway.expects(:ssl_request).returns(failed_authorize_response) response = @gateway.verify(@credit_card, @options) assert_failure response - assert_equal 'UNAUTHORIZED_API_CALL', response.message + assert_equal 'Do Not Honor', response.message end - def test_failed_verify - @gateway.expects(:ssl_request).returns(failed_authorize_response) + def test_successful_store_and_unstore + @gateway.expects(:ssl_request).twice.returns(successful_store_response, successful_unstore_response) - response = @gateway.verify(@credit_card, @options) + store = @gateway.store(@credit_card, @options) + assert_success store + assert customer_id = store.params.dig('data', 'id') + + unstore = @gateway.unstore(store.authorization) + assert_success unstore + assert_equal true, unstore.params.dig('data', 'deleted') + assert_equal customer_id, unstore.params.dig('data', 'id') + end + + def test_failed_purchase_without_customer_object + @options[:pm_type] = 'us_debit_visa_card' + @gateway.expects(:ssl_request).returns(failed_purchase_response) + response = @gateway.purchase(@amount, @credit_card, @options) assert_failure response - assert_equal 'Do Not Honor', response.message + assert_equal 'ERROR_PROCESSING_CARD - [05]', response.params['status']['error_code'] + end + + def test_successful_purchase_with_customer_object + stub_comms(@gateway, :ssl_request) do + @options[:pm_type] = 'us_debit_mastercard_card' + @gateway.purchase(@amount, @credit_card, @options) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + assert_match(/"name":"Jim Reynolds"/, data) + assert_match(/"email":"test@example.com"/, data) + assert_match(/"phone_number":"5555555555"/, data) + assert_match(/"customer":/, data) + end + end + + def test_successful_purchase_with_billing_address_phone_variations + stub_comms(@gateway, :ssl_request) do + @options[:pm_type] = 'us_debit_mastercard_card' + @gateway.purchase(@amount, @credit_card, { billing_address: { phone_number: '919.123.1234' } }) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + assert_match(/"phone_number":"9191231234"/, data) + end + + stub_comms(@gateway, :ssl_request) do + @options[:pm_type] = 'us_debit_mastercard_card' + @gateway.purchase(@amount, @credit_card, { billing_address: { phone: '919.123.1234' } }) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + assert_match(/"phone_number":"9191231234"/, data) + end + end + + def test_successful_store_with_customer_object + response = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"name":"Jim Reynolds"/, data) + assert_match(/"email":"test@example.com"/, data) + assert_match(/"phone_number":"5555555555"/, data) + end.respond_with(successful_store_response) + + assert_success response + end + + def test_payment_urls_correctly_nested_by_operation + response = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + request_body = JSON.parse(data) + assert_equal @options[:complete_payment_url], request_body['payment_method']['complete_payment_url'] + assert_equal @options[:error_payment_url], request_body['payment_method']['error_payment_url'] + end.respond_with(successful_store_response) + + assert_success response + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + request_body = JSON.parse(data) + assert_equal @options[:complete_payment_url], request_body['complete_payment_url'] + assert_equal @options[:error_payment_url], request_body['error_payment_url'] + end.respond_with(successful_store_response) + + assert_success response + end + + def test_purchase_with_customer_and_card_id + store = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.respond_with(successful_store_response) + + assert customer_id = store.params.dig('data', 'id') + assert card_id = store.params.dig('data', 'default_payment_method') + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, store.authorization, @options) + end.check_request do |_method, _endpoint, data, _headers| + request_body = JSON.parse(data) + assert_equal request_body['customer'], customer_id + assert_equal request_body['payment_method'], card_id + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure + options = { + three_d_secure: { + cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '5', + xid: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + } + } + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"cavv":"EHuWW9PiBkWvqE5juRwDzAUFBAk="/, data) + assert_match(/"eci":"5"/, data) + assert_match(/"xid":"TTBCSkVTa1ZpbDI1bjRxbGk5ODE="/, data) + end.respond_with(successful_authorize_response) + + assert_success response end def test_scrub @@ -159,6 +389,65 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_not_send_cvv_with_empty_value + @credit_card.verification_value = '' + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['payment_method']['fields']['cvv'] + end + end + + def test_not_send_cvv_with_nil_value + @credit_card.verification_value = nil + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['payment_method']['fields']['cvv'] + end + end + + def test_not_send_cvv_for_recurring_transactions + @options[:stored_credential] = { + reason_type: 'recurring', + network_transaction_id: '12345' + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['payment_method']['fields']['cvv'] + end + end + + def test_not_send_network_reference_id_for_recurring_transactions + @options[:stored_credential] = { + reason_type: 'recurring', + network_transaction_id: nil + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['payment_method']['fields']['network_reference_id'] + end + end + + def test_not_send_customer_object_for_recurring_transactions + @options[:stored_credential] = { + reason_type: 'recurring', + network_transaction_id: '12345' + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['customer'] + end + end + private def pre_scrubbed @@ -282,4 +571,22 @@ def failed_void_response {"status":{"error_code":"UNAUTHORIZED_API_CALL","status":"ERROR","message":"","response_code":"UNAUTHORIZED_API_CALL","operation_id":"12e59804-b742-44eb-aa49-4b722629faa8"}} ) end + + def successful_verify_response + %( + {"status":{"error_code":"","status":"SUCCESS","message":"","response_code":"","operation_id":"27385814-fc69-46fc-bbcc-2a5e0aac442d"},"data":{"id":"payment_2736748fec92a96c7c1280f7e46e2876","amount":0,"original_amount":0,"is_partial":false,"currency_code":"USD","country_code":"US","status":"ACT","description":"","merchant_reference_id":"","customer_token":"cus_c99aab5dae41102b0bb4276ab32e7777","payment_method":"card_5a07af7ff5c038eef4802ffb200fffa6","payment_method_data":{"id":"card_5a07af7ff5c038eef4802ffb200fffa6","type":"us_visa_card","category":"card","metadata":null,"image":"","webhook_url":"","supporting_documentation":"","next_action":"3d_verification","name":"Ryan Reynolds","last4":"1111","acs_check":"unchecked","cvv_check":"unchecked","bin_details":{"type":null,"brand":null,"level":null,"country":null,"bin_number":"411111"},"expiration_year":"35","expiration_month":"12","fingerprint_token":"ocfp_eb9edd24a3f3f59651aee0bd3d16201e"},"expiration":1653942478,"captured":false,"refunded":false,"refunded_amount":0,"receipt_email":"","redirect_url":"https://sandboxcheckout.rapyd.net/3ds-payment?token=payment_2736748fec92a96c7c1280f7e46e2876","complete_payment_url":"","error_payment_url":"","receipt_number":"","flow_type":"","address":null,"statement_descriptor":"N/A","transaction_id":"","created_at":1653337678,"metadata":{},"failure_code":"","failure_message":"","paid":false,"paid_at":0,"dispute":null,"refunds":null,"order":null,"outcome":null,"visual_codes":{},"textual_codes":{},"instructions":[],"ewallet_id":null,"ewallets":[],"payment_method_options":{},"payment_method_type":"us_visa_card","payment_method_type_category":"card","fx_rate":1,"merchant_requested_currency":null,"merchant_requested_amount":null,"fixed_side":"","payment_fees":null,"invoice":"","escrow":null,"group_payment":"","cancel_reason":null,"initiation_type":"customer_present","mid":"","next_action":"3d_verification","error_code":"","remitter_information":{}}} + ) + end + + def successful_store_response + %( + {"status":{"error_code":"","status":"SUCCESS","message":"","response_code":"","operation_id":"47e8bbbc-baa5-43c6-9395-df8a01645e91"},"data":{"id":"cus_4d8509d0997c7ce8aa1f63c19c1b6870","delinquent":false,"discount":null,"name":"Ryan Reynolds","default_payment_method":"card_94a3a70510109163a4eb438f06d82f78","description":"","email":"","phone_number":"","invoice_prefix":"","addresses":[],"payment_methods":{"data":[{"id":"card_94a3a70510109163a4eb438f06d82f78","type":"us_visa_card","category":"card","metadata":null,"image":"https://iconslib.rapyd.net/checkout/us_visa_card.png","webhook_url":"","supporting_documentation":"","next_action":"3d_verification","name":"Ryan Reynolds","last4":"1111","acs_check":"unchecked","cvv_check":"unchecked","bin_details":{"type":null,"brand":null,"level":null,"country":null,"bin_number":"411111"},"expiration_year":"35","expiration_month":"12","fingerprint_token":"ocfp_eb9edd24a3f3f59651aee0bd3d16201e","redirect_url":"https://sandboxcheckout.rapyd.net/3ds-payment?token=payment_f4ab1b25a09cbd769df05b30a29f71a4"}],"has_more":false,"total_count":1,"url":"/v1/customers/cus_4d8509d0997c7ce8aa1f63c19c1b6870/payment_methods"},"subscriptions":null,"created_at":1653487824,"metadata":{},"business_vat_id":"","ewallet":""}} + ) + end + + def successful_unstore_response + %( + {"status":{"error_code":"","status":"SUCCESS","message":"","response_code":"","operation_id":"6f7857f4-e063-4edb-ab93-da60c8563c52"},"data":{"deleted":true,"id":"cus_4d8509d0997c7ce8aa1f63c19c1b6870"}} + ) + end end diff --git a/test/unit/gateways/reach_test.rb b/test/unit/gateways/reach_test.rb new file mode 100644 index 00000000000..6a86450cccb --- /dev/null +++ b/test/unit/gateways/reach_test.rb @@ -0,0 +1,258 @@ +require 'test_helper' + +class ReachTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = ReachGateway.new(fixtures(:reach)) + @credit_card = credit_card + @amount = 100 + + @options = { + email: 'johndoe@reach.com', + order_id: '123', + currency: 'USD', + billing_address: { + address1: '1670', + address2: '1670 NW 82ND AVE', + city: 'Miami', + state: 'FL', + zip: '32191', + country: 'US' + } + } + end + + def test_required_merchant_id_and_secret + error = assert_raises(ArgumentError) { ReachGateway.new } + assert_equal 'Missing required parameter: merchant_id', error.message + end + + def test_supported_card_types + assert_equal ReachGateway.supported_cardtypes, %i[visa diners_club american_express jcb master discover maestro] + end + + def test_should_be_able_format_a_request + post = { + request: { someId: 'abc123' }, + card: { number: '12132323', name: 'John doe' } + } + + formatted = @gateway.send :format_and_sign, post + + refute_empty formatted[:signature] + assert_kind_of String, formatted[:request] + assert_kind_of String, formatted[:card] + + assert_equal 'abc123', JSON.parse(formatted[:request])['someId'] + assert_equal '12132323', JSON.parse(formatted[:card])['number'] + assert formatted[:signature].present? + end + + def test_properly_format_on_zero_decilmal + @options[:currency] = 'BYR' + stub_comms do + @gateway.authorize(1000, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_equal '10', request['Items'].first['ConsumerPrice'] + end.respond_with(successful_purchase_response) + end + + def test_successfully_build_a_purchase + stub_comms do + @gateway.authorize(1250, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + card = JSON.parse(URI.decode_www_form(data)[1][1]) + + # request + assert_equal request['ReferenceId'], @options[:order_id] + assert_equal request['Consumer']['Email'], @options[:email] + assert_equal request['ConsumerCurrency'], @options[:currency] + assert_equal request['Capture'], false + assert_equal '12.50', request['Items'].first['ConsumerPrice'] + + # card + assert_equal card['Number'], @credit_card.number + assert_equal card['Name'], @credit_card.name + assert_equal card['VerificationCode'], @credit_card.verification_value + end.respond_with(successful_purchase_response) + end + + def test_successfully_build_a_purchase_with_fingerprint + stub_comms do + @options[:device_fingerprint] = '54fd66c2-b5b5-4dbd-ab89-12a8b6177347' + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_equal request['DeviceFingerprint'], @options[:device_fingerprint] + end.respond_with(successful_purchase_response) + end + + def test_properly_set_capture_flag_on_purchase + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_equal true, request['Capture'] + end.respond_with(successful_purchase_response) + end + + def test_sending_item_sku_and_item_price + @options[:item_sku] = '1212121212' + @options[:item_quantity] = 250 + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + + # request + assert_equal request['Items'].first['Sku'], @options[:item_sku] + assert_equal request['Items'].first['Quantity'], @options[:item_quantity] + end.respond_with(successful_purchase_response) + end + + def test_successfull_retrieve_error_message + response = { 'response' => { 'Error' => { 'ReasonCode' => 'is an error' } } } + + message = @gateway.send(:message_from, response) + assert_equal 'is an error', message + end + + def test_safe_retrieve_error_message + response = { 'response' => { 'Error' => { 'Code' => 'is an error' } } } + + message = @gateway.send(:message_from, response) + assert_nil message + end + + def test_sucess_from_on_sucess_result + response = { 'response' => { OrderId: '' } } + + assert @gateway.send(:success_from, response) + end + + def test_sucess_from_on_failure + response = { 'response' => { 'Error' => 'is an error' } } + + refute @gateway.send(:success_from, response) + end + + def test_stored_credential + cases = + [ + { { initial_transaction: true, initiator: 'cardholder', reason_type: 'installment' } => 'CIT-Setup-Scheduled' }, + { { initial_transaction: true, initiator: 'cardholder', reason_type: 'unscheduled' } => 'CIT-Setup-Unscheduled-MIT' }, + { { initial_transaction: true, initiator: 'cardholder', reason_type: 'recurring' } => 'CIT-Setup-Unscheduled' }, + { { initial_transaction: false, initiator: 'cardholder', reason_type: 'unscheduled' } => 'CIT-Subsequent-Unscheduled' }, + { { initial_transaction: false, initiator: 'merchant', reason_type: 'recurring' } => 'MIT-Subsequent-Scheduled' }, + { { initial_transaction: false, initiator: 'merchant', reason_type: 'unscheduled' } => 'MIT-Subsequent-Unscheduled' } + ] + + cases.each do |stored_credential_case| + stored_credential_options = stored_credential_case.keys[0] + expected = stored_credential_case[stored_credential_options] + @options[:stored_credential] = stored_credential_options + stub_comms do + @gateway.expects(:ssl_request).returns(succesful_query_response) + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_equal expected, request['PaymentModel'] + end.respond_with(successful_purchase_response) + end + end + + def test_stored_credential_with_no_store_credential_parameters + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_equal 'CIT-One-Time', request['PaymentModel'] + end.respond_with(successful_purchase_response) + end + + def test_stored_credential_with_wrong_combination_stored_credential_paramaters + @options[:stored_credential] = { initiator: 'merchant', initial_transaction: true, reason_type: 'unscheduled' } + @gateway.expects(:get_network_payment_reference).returns(stub(message: 'abc123', "success?": true)) + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_empty request['PaymentModel'] + end.respond_with(successful_purchase_response) + end + + def test_stored_credential_with_at_lest_one_stored_credential_paramaters_nil + @options[:stored_credential] = { initiator: 'merchant', initial_transaction: true, reason_type: nil } + @gateway.expects(:get_network_payment_reference).returns(stub(message: 'abc123', "success?": true)) + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(URI.decode_www_form(data)[0][1]) + assert_empty request['PaymentModel'] + end.respond_with(successful_purchase_response) + end + + def test_scrub + assert @gateway.supports_scrubbing? + + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def successful_purchase_response + 'response=%7B%22OrderId%22%3A%22e8f8c529-15c7-46c1-b28b-9d43bb5efe92%22%2C%22UnderReview%22%3Afalse%2C%22Expiry%22%3A%222022-11-03T12%3A47%3A21Z%22%2C%22Authorized%22%3Atrue%2C%22Completed%22%3Afalse%2C%22Captured%22%3Afalse%7D&signature=JqLa7Y68OYRgRcA5ALHOZwXXzdZFeNzqHma2RT2JWAg%3D' + end + + def succesful_query_response + 'response=%7B%22Meta%22%3A%20null%2C%20%22Rate%22%3A%201.000000000000%2C%20%22Items%22%3A%20%5B%7B%22Sku%22%3A%20%22RLaP7OsSZjbR2pJK%22%2C%20%22Quantity%22%3A%201%2C%20%22ConsumerPrice%22%3A%20100.00%2C%20%22MerchantPrice%22%3A%20100.00%7D%5D%2C%20%22Store%22%3A%20null%2C%20%22Times%22%3A%20%7B%22Created%22%3A%20%222022-12-05T17%3A48%3A18.830991Z%22%2C%20%22Processed%22%3A%20null%2C%20%22Authorized%22%3A%20%222022-12-05T17%3A48%3A19.855608Z%22%7D%2C%20%22Action%22%3A%20null%2C%20%22Expiry%22%3A%20%222022-12-12T17%3A48%3A19.855608Z%22%2C%20%22Reason%22%3A%20null%2C%20%22Charges%22%3A%20null%2C%20%22OrderId%22%3A%20%226ec68268-a4a5-44dd-8997-e76df4aa9c97%22%2C%20%22Payment%22%3A%20%7B%22Class%22%3A%20%22Card%22%2C%20%22Expiry%22%3A%20%222030-03%22%2C%20%22Method%22%3A%20%22VISA%22%2C%20%22AccountIdentifier%22%3A%20%22444433******1111%22%2C%20%22NetworkPaymentReference%22%3A%20%22546646904394415%22%7D%2C%20%22Refunds%22%3A%20%5B%5D%2C%20%22Consumer%22%3A%20%7B%22City%22%3A%20%22Miami%22%2C%20%22Name%22%3A%20%22Longbob%20Longsen%22%2C%20%22Email%22%3A%20%22johndoe%40reach.com%22%2C%20%22Address%22%3A%20%221670%22%2C%20%22Country%22%3A%20%22US%22%2C%20%22EffectiveIpAddress%22%3A%20%22181.78.14.203%22%7D%2C%20%22Shipping%22%3A%20null%2C%20%22Consignee%22%3A%20null%2C%20%22Discounts%22%3A%20null%2C%20%22Financing%22%3A%20null%2C%20%22Chargeback%22%3A%20false%2C%20%22ContractId%22%3A%20null%2C%20%22MerchantId%22%3A%20%22testMerchantId%22%2C%20%22OrderState%22%3A%20%22PaymentAuthorized%22%2C%20%22RateOfferId%22%3A%20%22c754012f-e0fc-4630-9cb5-11c3450f462e%22%2C%20%22ReferenceId%22%3A%20%22123%22%2C%20%22UnderReview%22%3A%20false%2C%20%22ConsumerTotal%22%3A%20100.00%2C%20%22MerchantTotal%22%3A%20100.00%2C%20%22TransactionId%22%3A%20%22e08f6501-2607-4be1-9dba-97d6780dfe9a%22%2C%20%22ConsumerCurrency%22%3A%20%22USD%22%7D&signature=no%2BEojgxrO5JK4wt4EWtbuY9M7h1eVQ9SLezu10X%2Bn4%3D' + end + + def pre_scrubbed + <<-PRE_SCRUBBED + <- "POST /v2.21/checkout HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: checkout.rch.how\r\nContent-Length: 756\r\n\r\n" + <- "request=%7B%22MerchantId%22%3A%22Some-30value-4for3-9test35-f93086cd7crednet1%22%2C%22ReferenceId%22%3A%22123%22%2C%22ConsumerCurrency%22%3A%22USD%22%2C%22Capture%22%3Atrue%2C%22PaymentMethod%22%3A%22VISA%22%2C%22Items%22%3A%5B%7B%22Sku%22%3A%22d99oJA8rkwgQANFJ%22%2C%22ConsumerPrice%22%3A100%2C%22Quantity%22%3A1%7D%5D%2C%22ViaAgent%22%3Atrue%2C%22Consumer%22%3A%7B%22Name%22%3A%22Longbob+Longsen%22%2C%22Email%22%3A%22johndoe%40reach.com%22%2C%22Address%22%3A%221670%22%2C%22City%22%3A%22Miami%22%2C%22Country%22%3A%22US%22%7D%7D&card=%7B%22Name%22%3A%22Longbob+Longsen%22%2C%22Number%22%3A%224444333322221111%22%2C%22Expiry%22%3A%7B%22Month%22%3A3%2C%22Year%22%3A2030%7D%2C%22VerificationCode%22%3A737%7D&signature=5nimSignatUre%3D" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Thu, 03 Nov 2022 23:04:01 GMT\r\n" + -> "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n" + -> "Content-Length: 235\r\n" + -> "Connection: close\r\n" + -> "Server: ipCheckoutApi/unreleased ibiHttpServer\r\n" + -> "Strict-Transport-Security: max-age=60000\r\n" + -> "Cache-Control: no-cache\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "\r\n" + reading 235 bytes... + -> "response=%7B%22OrderId%22%3A%22621a0c76-69fb-4c05-854a-e7e731759ad3%22%2C%22UnderReview%22%3Afalse%2C%22Authorized%22%3Atrue%2C%22Completed%22%3Afalse%2C%22Captured%22%3Afalse%7D&signature=23475signature23123%3D" + read 235 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SRCUBBED + <- "POST /v2.21/checkout HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: checkout.rch.how\r\nContent-Length: 756\r\n\r\n" + <- "request=%7B%22MerchantId%22%3A%22[FILTERED]%22%2C%22ReferenceId%22%3A%22123%22%2C%22ConsumerCurrency%22%3A%22USD%22%2C%22Capture%22%3Atrue%2C%22PaymentMethod%22%3A%22VISA%22%2C%22Items%22%3A%5B%7B%22Sku%22%3A%22d99oJA8rkwgQANFJ%22%2C%22ConsumerPrice%22%3A100%2C%22Quantity%22%3A1%7D%5D%2C%22ViaAgent%22%3Atrue%2C%22Consumer%22%3A%7B%22Name%22%3A%22Longbob+Longsen%22%2C%22Email%22%3A%22johndoe%40reach.com%22%2C%22Address%22%3A%221670%22%2C%22City%22%3A%22Miami%22%2C%22Country%22%3A%22US%22%7D%7D&card=%7B%22Name%22%3A%22Longbob+Longsen%22%2C%22Number%22%3A%22[FILTERED]%22%2C%22Expiry%22%3A%7B%22Month%22%3A3%2C%22Year%22%3A2030%7D%2C%22VerificationCode%22%3A[FILTERED]%7D&signature=[FILTERED]" + -> "HTTP/1.1 200 OK\r\n" + -> "Date: Thu, 03 Nov 2022 23:04:01 GMT\r\n" + -> "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n" + -> "Content-Length: 235\r\n" + -> "Connection: close\r\n" + -> "Server: ipCheckoutApi/unreleased ibiHttpServer\r\n" + -> "Strict-Transport-Security: max-age=60000\r\n" + -> "Cache-Control: no-cache\r\n" + -> "Access-Control-Allow-Origin: *\r\n" + -> "\r\n" + reading 235 bytes... + -> "response=%7B%22OrderId%22%3A%22621a0c76-69fb-4c05-854a-e7e731759ad3%22%2C%22UnderReview%22%3Afalse%2C%22Authorized%22%3Atrue%2C%22Completed%22%3Afalse%2C%22Captured%22%3Afalse%7D&signature=[FILTERED]" + read 235 bytes + Conn close + POST_SRCUBBED + end +end diff --git a/test/unit/gateways/realex_test.rb b/test/unit/gateways/realex_test.rb index 84f7da6bd99..9fc3358c256 100644 --- a/test/unit/gateways/realex_test.rb +++ b/test/unit/gateways/realex_test.rb @@ -5,8 +5,7 @@ class RealexTest < Test::Unit::TestCase class ActiveMerchant::Billing::RealexGateway # For the purposes of testing, lets redefine some protected methods as public. - public :build_purchase_or_authorization_request, :build_refund_request, :build_void_request, - :build_capture_request, :build_verify_request, :build_credit_request + public :build_purchase_or_authorization_request, :build_refund_request, :build_void_request, :build_capture_request, :build_verify_request, :build_credit_request end def setup diff --git a/test/unit/gateways/redsys_sha256_test.rb b/test/unit/gateways/redsys_sha256_test.rb index 5573d5f3cdc..66dae34a2c0 100644 --- a/test/unit/gateways/redsys_sha256_test.rb +++ b/test/unit/gateways/redsys_sha256_test.rb @@ -391,7 +391,7 @@ def test_default_currency end def test_supported_countries - assert_equal ['ES'], RedsysGateway.supported_countries + assert_equal %w[ES FR GB IT PL PT], RedsysGateway.supported_countries end def test_supported_cardtypes @@ -460,11 +460,11 @@ def generate_order_id # one with card and another without. def purchase_request - 'entrada=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%3CREQUEST%3E%3CDATOSENTRADA%3E%3CDS_Version%3E0.1%3C%2FDS_Version%3E%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%3CDS_MERCHANT_ORDER%3E144742736014%3C%2FDS_MERCHANT_ORDER%3E%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%3CDS_MERCHANT_PAN%3E4548812049400004%3C%2FDS_MERCHANT_PAN%3E%3CDS_MERCHANT_EXPIRYDATE%3E1709%3C%2FDS_MERCHANT_EXPIRYDATE%3E%3CDS_MERCHANT_CVV2%3E123%3C%2FDS_MERCHANT_CVV2%3E%3C%2FDATOSENTRADA%3E%3CDS_SIGNATUREVERSION%3EHMAC_SHA256_V1%3C%2FDS_SIGNATUREVERSION%3E%3CDS_SIGNATURE%3Eq9QH2P%2B4qm8w%2FS85KRPVaepWOrOT2RXlEmyPUce5XRM%3D%3C%2FDS_SIGNATURE%3E%3C%2FREQUEST%3E' + 'entrada=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%3CREQUEST%3E%3CDATOSENTRADA%3E%3CDS_Version%3E0.1%3C%2FDS_Version%3E%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%3CDS_MERCHANT_ORDER%3E144742736014%3C%2FDS_MERCHANT_ORDER%3E%3CDS_MERCHANT_TRANSACTIONTYPE%3E0%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%3CDS_MERCHANT_PAN%3E4548812049400004%3C%2FDS_MERCHANT_PAN%3E%3CDS_MERCHANT_EXPIRYDATE%3E1709%3C%2FDS_MERCHANT_EXPIRYDATE%3E%3CDS_MERCHANT_CVV2%3E123%3C%2FDS_MERCHANT_CVV2%3E%3C%2FDATOSENTRADA%3E%3CDS_SIGNATUREVERSION%3EHMAC_SHA256_V1%3C%2FDS_SIGNATUREVERSION%3E%3CDS_SIGNATURE%3Ef46TQxKLJJ6SjcETDp%2Bul92Qsb5kVve2QzGnZMj8JkI%3D%3C%2FDS_SIGNATURE%3E%3C%2FREQUEST%3E' end def purchase_request_with_credit_card_token - 'entrada=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%3CREQUEST%3E%3CDATOSENTRADA%3E%3CDS_Version%3E0.1%3C%2FDS_Version%3E%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%3CDS_MERCHANT_ORDER%3E144742884282%3C%2FDS_MERCHANT_ORDER%3E%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%3CDS_MERCHANT_IDENTIFIER%3E3126bb8b80a79e66eb1ecc39e305288b60075f86%3C%2FDS_MERCHANT_IDENTIFIER%3E%3CDS_MERCHANT_DIRECTPAYMENT%3Etrue%3C%2FDS_MERCHANT_DIRECTPAYMENT%3E%3C%2FDATOSENTRADA%3E%3CDS_SIGNATUREVERSION%3EHMAC_SHA256_V1%3C%2FDS_SIGNATUREVERSION%3E%3CDS_SIGNATURE%3EN0tYMrHGf1PmmJ7WIiRONdqbIGmyhaV%2BhP4acTyfJYE%3D%3C%2FDS_SIGNATURE%3E%3C%2FREQUEST%3E' + 'entrada=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%3CREQUEST%3E%3CDATOSENTRADA%3E%3CDS_Version%3E0.1%3C%2FDS_Version%3E%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%3CDS_MERCHANT_AMOUNT%3E100%3C%2FDS_MERCHANT_AMOUNT%3E%3CDS_MERCHANT_ORDER%3E144742884282%3C%2FDS_MERCHANT_ORDER%3E%3CDS_MERCHANT_TRANSACTIONTYPE%3E0%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%3CDS_MERCHANT_IDENTIFIER%3E3126bb8b80a79e66eb1ecc39e305288b60075f86%3C%2FDS_MERCHANT_IDENTIFIER%3E%3CDS_MERCHANT_DIRECTPAYMENT%3Etrue%3C%2FDS_MERCHANT_DIRECTPAYMENT%3E%3C%2FDATOSENTRADA%3E%3CDS_SIGNATUREVERSION%3EHMAC_SHA256_V1%3C%2FDS_SIGNATUREVERSION%3E%3CDS_SIGNATURE%3Eeozf9m%2FmDx7JKtcJSPvUa%2FdCZQmzzEAU2nrOVD84fp4%3D%3C%2FDS_SIGNATURE%3E%3C%2FREQUEST%3E' end def successful_purchase_response diff --git a/test/unit/gateways/redsys_test.rb b/test/unit/gateways/redsys_test.rb index 3cbdce4c2e2..605d077e38a 100644 --- a/test/unit/gateways/redsys_test.rb +++ b/test/unit/gateways/redsys_test.rb @@ -59,7 +59,7 @@ def test_successful_purchase_with_stored_credentials assert_success initial_res assert_equal 'Transaction Approved', initial_res.message assert_equal '2012102122020', initial_res.params['ds_merchant_cof_txnid'] - network_transaction_id = initial_res.params['Ds_Merchant_Cof_Txnid'] + network_transaction_id = initial_res.params['ds_merchant_cof_txnid'] @gateway.expects(:ssl_post).returns(successful_purchase_used_stored_credential_response) used_options = { @@ -76,6 +76,128 @@ def test_successful_purchase_with_stored_credentials assert_equal '561350', res.params['ds_authorisationcode'] end + def test_successful_purchase_with_stored_credentials_for_merchant_initiated_transactions + @gateway.expects(:ssl_post).with( + anything, + all_of( + includes(CGI.escape('0')), + includes(CGI.escape('1001')), + includes(CGI.escape('123')), + includes(CGI.escape('S')), + includes(CGI.escape('R')), + includes(CGI.escape('4242424242424242')), + includes(CGI.escape('2409')), + includes(CGI.escape('123')), + includes(CGI.escape('false')), + Not(includes(CGI.escape(''))), + Not(includes(CGI.escape(''))) + ), + anything + ).returns(successful_purchase_initial_stored_credential_response) + + initial_options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'recurring' + } + ) + initial_res = @gateway.purchase(123, credit_card, initial_options) + assert_success initial_res + assert_equal 'Transaction Approved', initial_res.message + assert_equal '2012102122020', initial_res.params['ds_merchant_cof_txnid'] + network_transaction_id = initial_res.params['ds_merchant_cof_txnid'] + + @gateway.expects(:ssl_post).with( + anything, + all_of( + includes('0'), + includes('1002'), + includes('123'), + includes('N'), + includes('R'), + includes('4242424242424242'), + includes('2409'), + includes('123'), + includes('true'), + includes('MIT'), + includes("#{network_transaction_id}") + ), + anything + ).returns(successful_purchase_used_stored_credential_response) + used_options = { + order_id: '1002', + sca_exemption: 'MIT', + stored_credential: { + initial_transaction: false, + reason_type: 'recurring', + network_transaction_id: network_transaction_id + } + } + res = @gateway.purchase(123, credit_card, used_options) + assert_success res + assert_equal 'Transaction Approved', res.message + assert_equal '561350', res.params['ds_authorisationcode'] + end + + def test_successful_purchase_with_stored_credentials_for_merchant_initiated_transactions_with_card_tokens + @gateway.expects(:ssl_post).with( + anything, + all_of( + includes(CGI.escape('0')), + includes(CGI.escape('1001')), + includes(CGI.escape('123')), + includes(CGI.escape('S')), + includes(CGI.escape('R')), + includes(CGI.escape('77bff3a969d6f97b2ec815448cdcff453971f573')), + includes(CGI.escape('false')), + Not(includes(CGI.escape(''))), + Not(includes(CGI.escape(''))) + ), + anything + ).returns(successful_purchase_initial_stored_credential_response) + + initial_options = @options.merge( + stored_credential: { + initial_transaction: true, + reason_type: 'recurring' + } + ) + initial_res = @gateway.purchase(123, '77bff3a969d6f97b2ec815448cdcff453971f573', initial_options) + assert_success initial_res + assert_equal 'Transaction Approved', initial_res.message + assert_equal '2012102122020', initial_res.params['ds_merchant_cof_txnid'] + network_transaction_id = initial_res.params['ds_merchant_cof_txnid'] + + @gateway.expects(:ssl_post).with( + anything, + all_of( + includes('0'), + includes('1002'), + includes('123'), + includes('N'), + includes('R'), + includes('77bff3a969d6f97b2ec815448cdcff453971f573'), + includes('true'), + includes('MIT'), + includes("#{network_transaction_id}") + ), + anything + ).returns(successful_purchase_used_stored_credential_response) + used_options = { + order_id: '1002', + sca_exemption: 'MIT', + stored_credential: { + initial_transaction: false, + reason_type: 'recurring', + network_transaction_id: network_transaction_id + } + } + res = @gateway.purchase(123, '77bff3a969d6f97b2ec815448cdcff453971f573', used_options) + assert_success res + assert_equal 'Transaction Approved', res.message + assert_equal '561350', res.params['ds_authorisationcode'] + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) res = @gateway.purchase(123, credit_card, @options) @@ -224,7 +346,7 @@ def test_default_currency end def test_supported_countries - assert_equal ['ES'], RedsysGateway.supported_countries + assert_equal %w[ES FR GB IT PL PT], RedsysGateway.supported_countries end def test_supported_cardtypes @@ -285,11 +407,11 @@ def test_whitespace_string_cvv_transcript_scrubbing # one with card and another without. def purchase_request - "entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E123%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E1001%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3Eb98b606a6a588d8c45c239f244160efbbe30b4a8%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%0A++%3CDS_MERCHANT_PAN%3E4242424242424242%3C%2FDS_MERCHANT_PAN%3E%0A++%3CDS_MERCHANT_EXPIRYDATE%3E#{(Time.now.year + 1).to_s.slice(2, 2)}09%3C%2FDS_MERCHANT_EXPIRYDATE%3E%0A++%3CDS_MERCHANT_CVV2%3E123%3C%2FDS_MERCHANT_CVV2%3E%0A%3C%2FDATOSENTRADA%3E%0A" + "entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E123%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E1001%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3E0%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3E0b930082f7905d7dba3d83be4d4331b8acd57624%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_TITULAR%3ELongbob+Longsen%3C%2FDS_MERCHANT_TITULAR%3E%0A++%3CDS_MERCHANT_PAN%3E4242424242424242%3C%2FDS_MERCHANT_PAN%3E%0A++%3CDS_MERCHANT_EXPIRYDATE%3E#{(Time.now.year + 1).to_s.slice(2, 2)}09%3C%2FDS_MERCHANT_EXPIRYDATE%3E%0A++%3CDS_MERCHANT_CVV2%3E123%3C%2FDS_MERCHANT_CVV2%3E%0A%3C%2FDATOSENTRADA%3E%0A" end def purchase_request_with_credit_card_token - 'entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E123%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E1001%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3EA%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3Ecbcc0dee5724cd3fff08bbd4371946a0599c7fb9%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_IDENTIFIER%3E77bff3a969d6f97b2ec815448cdcff453971f573%3C%2FDS_MERCHANT_IDENTIFIER%3E%0A++%3CDS_MERCHANT_DIRECTPAYMENT%3Etrue%3C%2FDS_MERCHANT_DIRECTPAYMENT%3E%0A%3C%2FDATOSENTRADA%3E%0A' + 'entrada=%3CDATOSENTRADA%3E%0A++%3CDS_Version%3E0.1%3C%2FDS_Version%3E%0A++%3CDS_MERCHANT_CURRENCY%3E978%3C%2FDS_MERCHANT_CURRENCY%3E%0A++%3CDS_MERCHANT_AMOUNT%3E123%3C%2FDS_MERCHANT_AMOUNT%3E%0A++%3CDS_MERCHANT_ORDER%3E1001%3C%2FDS_MERCHANT_ORDER%3E%0A++%3CDS_MERCHANT_TRANSACTIONTYPE%3E0%3C%2FDS_MERCHANT_TRANSACTIONTYPE%3E%0A++%3CDS_MERCHANT_PRODUCTDESCRIPTION%2F%3E%0A++%3CDS_MERCHANT_TERMINAL%3E1%3C%2FDS_MERCHANT_TERMINAL%3E%0A++%3CDS_MERCHANT_MERCHANTCODE%3E091952713%3C%2FDS_MERCHANT_MERCHANTCODE%3E%0A++%3CDS_MERCHANT_MERCHANTSIGNATURE%3E0c56bb3edd0cae65ef16c96c61c5ecd306973d2f%3C%2FDS_MERCHANT_MERCHANTSIGNATURE%3E%0A++%3CDS_MERCHANT_IDENTIFIER%3E77bff3a969d6f97b2ec815448cdcff453971f573%3C%2FDS_MERCHANT_IDENTIFIER%3E%0A++%3CDS_MERCHANT_DIRECTPAYMENT%3Etrue%3C%2FDS_MERCHANT_DIRECTPAYMENT%3E%0A%3C%2FDATOSENTRADA%3E%0A' end def successful_purchase_response @@ -301,7 +423,7 @@ def successful_purchase_response_with_credit_card_token end def successful_purchase_initial_stored_credential_response - "00.11239781001989D357BCC9EF0962A456C51422C4FAF4BF4399F9195271310000561350A01724201210212202013" + "00.11239781001989D357BCC9EF0962A456C51422C4FAF4BF4399F9195271310000561350A0177bff3a969d6f97b2ec815448cdcff453971f573724201210212202013" end def successful_purchase_used_stored_credential_response diff --git a/test/unit/gateways/safe_charge_test.rb b/test/unit/gateways/safe_charge_test.rb index 2fdc49f11da..796ee649c8a 100644 --- a/test/unit/gateways/safe_charge_test.rb +++ b/test/unit/gateways/safe_charge_test.rb @@ -203,6 +203,26 @@ def test_successful_refund assert_equal 'Success', response.message end + def test_successful_unreferenced_refund + refund = stub_comms do + @gateway.refund(@amount, 'auth|transaction_id|token|month|year|amount|currency', @options.merge(unreferenced_refund: true)) + end.check_request do |_endpoint, data, _headers| + assert_equal(data.split('&').include?('sg_TransactionID=transaction_id'), false) + end.respond_with(successful_refund_response) + + assert_success refund + end + + def test_successful_refund_without_unreferenced_refund + refund = stub_comms do + @gateway.refund(@amount, 'auth|transaction_id|token|month|year|amount|currency', @options) + end.check_request do |_endpoint, data, _headers| + assert_equal(data.split('&').include?('sg_TransactionID=transaction_id'), true) + end.respond_with(successful_refund_response) + + assert_success refund + end + def test_failed_refund @gateway.expects(:ssl_post).returns(failed_refund_response) @@ -219,6 +239,16 @@ def test_successful_credit assert_equal 'Success', response.message end + def test_credit_sends_addtional_info + stub_comms do + @gateway.credit(@amount, @credit_card, @options.merge(email: 'test@example.com')) + end.check_request do |_endpoint, data, _headers| + assert_match(/sg_FirstName=Longbob/, data) + assert_match(/sg_LastName=Longsen/, data) + assert_match(/sg_Email/, data) + end.respond_with(successful_credit_response) + end + def test_failed_credit @gateway.expects(:ssl_post).returns(failed_credit_response) diff --git a/test/unit/gateways/sage_pay_test.rb b/test/unit/gateways/sage_pay_test.rb index 2a258e4c043..8f915a1f6c4 100644 --- a/test/unit/gateways/sage_pay_test.rb +++ b/test/unit/gateways/sage_pay_test.rb @@ -316,9 +316,7 @@ def test_successful_authorization_and_capture_and_refund assert_success capture refund = stub_comms do - @gateway.refund(@amount, capture.authorization, - order_id: generate_unique_id, - description: 'Refund txn') + @gateway.refund(@amount, capture.authorization, order_id: generate_unique_id, description: 'Refund txn') end.respond_with(successful_refund_response) assert_success refund end @@ -332,6 +330,15 @@ def test_repeat_purchase_with_reference_token end.respond_with(successful_purchase_response) end + def test_repeat_purchase_from_reference_purchase + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, '9a3c5a71ef733ce56a9b03754763da2c;{4B98024C-5D40-4F5C-4E19-A8D07EBFC5AD};14575233;7NJB98CZSG;repeat', @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/RelatedVPSTxId=%7B4B98024C-5D40-4F5C-4E19-A8D07EBFC5AD%/, data) + assert_match(/TxType=REPEAT/, data) + end.respond_with(successful_purchase_response) + end + private def purchase_with_options(optional) diff --git a/test/unit/gateways/securion_pay_test.rb b/test/unit/gateways/securion_pay_test.rb index 020dbdf4b27..5d3dc3b115b 100644 --- a/test/unit/gateways/securion_pay_test.rb +++ b/test/unit/gateways/securion_pay_test.rb @@ -36,7 +36,6 @@ def test_successful_store response = @gateway.store(@credit_card, @options) assert_success response - assert_match %r(^cust_\w+$), response.authorization assert_equal 'customer', response.params['objectType'] assert_match %r(^card_\w+$), response.params['cards'][0]['id'] assert_equal 'card', response.params['cards'][0]['objectType'] @@ -44,7 +43,8 @@ def test_successful_store @gateway.expects(:ssl_post).returns(successful_authorize_response) @gateway.expects(:ssl_post).returns(successful_void_response) - @options[:customer_id] = response.authorization + @options[:customer_id] = response.params['cards'][0]['customerId'] + response = @gateway.store(@new_credit_card, @options) assert_success response assert_match %r(^card_\w+$), response.params['card']['id'] @@ -394,6 +394,15 @@ def test_declined_request assert_equal 'char_mApucpvVbCJgo7x09Je4n9gC', response.params['error']['chargeId'] end + def test_amount_currency_gets_downcased + @options[:currency] = 'USD' + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'usd', CGI.parse(data)['currency'].first + end.respond_with(successful_purchase_response) + end + private def pre_scrubbed diff --git a/test/unit/gateways/shift4_test.rb b/test/unit/gateways/shift4_test.rb new file mode 100644 index 00000000000..267de4254c9 --- /dev/null +++ b/test/unit/gateways/shift4_test.rb @@ -0,0 +1,1072 @@ +require 'test_helper' + +class Shift4Test < Test::Unit::TestCase + include CommStub + def setup + @gateway = Shift4Gateway.new(client_guid: '123456', auth_token: 'abcder123') + @credit_card = credit_card('4000100011112224', verification_value: '333', first_name: 'John', last_name: 'Doe') + @amount = 5 + @options = {} + @extra_options = { + clerk_id: '1576', + notes: 'test notes', + tax: '2', + customer_reference: 'D019D09309F2', + destination_postal_code: '94719', + product_descriptors: %w(Hamburger Fries Soda Cookie), + order_id: '123456' + } + @customer_address = { + address1: '123 Street', + zip: '94901' + } + end + + def test_successful_capture + response = stub_comms do + @gateway.capture(@amount, '1111g66gw3ryke06', @options) + end.check_request do |_endpoint, data, headers| + request = JSON.parse(data) + assert_nil request['card']['present'], 'N' + assert_nil request['card']['entryMode'] + assert_nil headers['Invoice'] + end.respond_with(successful_capture_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + assert_equal response_result(response)['card']['token']['value'].present?, true + end + + def test_successful_authorize + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.respond_with(successful_authorize_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + assert_equal response_result(response)['card']['token']['value'].present?, true + end + + def test_successful_purchase + response = stub_comms do + @gateway.purchase(@amount, '1111g66gw3ryke06', @options) + end.respond_with(successful_purchase_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + assert_equal response_result(response)['card']['token']['value'].present?, true + end + + def test_successful_purchase_with_extra_fields + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['clerk']['numericId'], @extra_options[:clerk_id] + assert_equal request['transaction']['notes'], @extra_options[:notes] + assert_equal request['transaction']['vendorReference'], @extra_options[:order_id] + assert_equal request['amount']['tax'], @extra_options[:tax].to_f + assert_equal request['amount']['total'], (@amount / 100.0).to_s + assert_equal request['transaction']['purchaseCard']['customerReference'], @extra_options[:customer_reference] + assert_equal request['transaction']['purchaseCard']['destinationPostalCode'], @extra_options[:destination_postal_code] + assert_equal request['transaction']['purchaseCard']['productDescriptors'], @extra_options[:product_descriptors] + end.respond_with(successful_purchase_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + end + + def test_successful_purchase_with_customer_details + customer = { billing_address: @customer_address, ip: '127.0.0.1', email: 'test@test.com' } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(customer)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['customer']['addressLine1'], @customer_address[:address1] + assert_equal request['customer']['postalCode'], @customer_address[:zip] + assert_equal request['customer']['emailAddress'], customer[:email] + assert_equal request['customer']['ipAddress'], customer[:ip] + assert_equal request['customer']['firstName'], @credit_card.first_name + assert_equal request['customer']['lastName'], @credit_card.last_name + end.respond_with(successful_purchase_response) + + customer[:billing_address][:zip] = nil + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(customer)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['customer']['postalCode'] + end.respond_with(successful_purchase_response) + + customer[:billing_address][:zip] = '' + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(customer)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['customer']['postalCode'] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_stored_credential_framework + stored_credential_options = { + initial_transaction: true, + reason_type: 'recurring' + } + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data)['transaction'] + assert_equal request['cardOnFile']['usageIndicator'], '01' + assert_equal request['cardOnFile']['indicator'], '01' + assert_equal request['cardOnFile']['scheduledIndicator'], '01' + assert_nil request['cardOnFile']['transactionId'] + end.respond_with(successful_purchase_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + + stored_credential_options = { + reason_type: 'recurring', + network_transaction_id: '123abcdefg' + } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data)['transaction'] + assert_equal request['cardOnFile']['usageIndicator'], '02' + assert_equal request['cardOnFile']['indicator'], '01' + assert_equal request['cardOnFile']['scheduledIndicator'], '01' + assert_equal request['cardOnFile']['transactionId'], stored_credential_options[:network_transaction_id] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_card_on_file_fields + card_on_file_fields = { + usage_indicator: '01', + indicator: '02', + scheduled_indicator: '01' + } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(card_on_file_fields)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data)['transaction'] + assert_equal request['cardOnFile']['usageIndicator'], card_on_file_fields[:usage_indicator] + assert_equal request['cardOnFile']['indicator'], card_on_file_fields[:indicator] + assert_equal request['cardOnFile']['scheduledIndicator'], card_on_file_fields[:scheduled_indicator] + assert_nil request['cardOnFile']['transactionId'] + end.respond_with(successful_purchase_response) + + card_on_file_fields = { + usage_indicator: '02', + indicator: '01', + scheduled_indicator: '02', + transaction_id: 'TXID00001293' + } + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(card_on_file_fields)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data)['transaction'] + assert_equal request['cardOnFile']['usageIndicator'], card_on_file_fields[:usage_indicator] + assert_equal request['cardOnFile']['indicator'], card_on_file_fields[:indicator] + assert_equal request['cardOnFile']['scheduledIndicator'], card_on_file_fields[:scheduled_indicator] + assert_equal request['cardOnFile']['transactionId'], card_on_file_fields[:transaction_id] + end.respond_with(successful_purchase_response) + end + + def test_card_on_file_fields_and_stored_credential_framework_combined + card_on_file_fields = { + usage_indicator: '02', + indicator: '02', + scheduled_indicator: '02' + } + stored_credential_options = { + initial_transaction: true, + reason_type: 'recurring' + } + @options[:stored_credential] = stored_credential_options + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(card_on_file_fields)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data)['transaction'] + assert_equal request['cardOnFile']['usageIndicator'], card_on_file_fields[:usage_indicator] + assert_equal request['cardOnFile']['indicator'], card_on_file_fields[:indicator] + assert_equal request['cardOnFile']['scheduledIndicator'], card_on_file_fields[:scheduled_indicator] + assert_nil request['cardOnFile']['transactionId'] + end.respond_with(successful_purchase_response) + end + + def test_successful_store + response = stub_comms do + @gateway.store(@credit_card, @options.merge(@extra_options.except(:tax))) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['card']['entryMode'] + assert_nil request['clerk'] + end.respond_with(successful_store_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + end + + def test_successful_refund + response = stub_comms do + @gateway.refund(@amount, '1111g66gw3ryke06', @options.merge!(invoice: '4666309473', expiration_date: '1235')) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['card']['present'], 'N' + assert_equal request['card']['expirationDate'], '1235' + assert_nil request['card']['entryMode'] + assert_nil request['customer'] + end.respond_with(successful_refund_response) + + assert response.success? + assert_equal response.message, 'Transaction successful' + end + + def test_successful_credit + stub_comms do + @gateway.refund(@amount, @credit_card, @options.merge!(invoice: '4666309473', expiration_date: '1235')) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['card']['present'], 'N' + assert_equal request['card']['expirationDate'], '0924' + assert_nil request['card']['entryMode'] + assert_nil request['customer'] + end.respond_with(successful_refund_response) + end + + def test_successful_void + @gateway.expects(:ssl_request).returns(successful_void_response) + response = @gateway.void('123') + + assert response.success? + assert_equal response.message, 'Transaction successful' + end + + def test_failed_purchase + @gateway.expects(:ssl_request).returns(failed_purchase_response) + + response = @gateway.purchase(@amount, 'abc', @options) + assert_failure response + assert_nil response.authorization + end + + def test_failed_authorize + @gateway.expects(:ssl_request).returns(failed_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_nil response.authorization + assert response.test? + end + + def test_failed_capture + @gateway.expects(:ssl_request).returns(failed_capture_response) + + response = @gateway.capture(@amount, 'abc', @options) + assert_failure response + assert_nil response.authorization + assert response.test? + end + + def test_failed_refund + @gateway.expects(:ssl_request).returns(failed_refund_response) + + response = @gateway.refund(@amount, 'abc', @options) + assert_failure response + assert_nil response.authorization + assert response.test? + end + + def test_failed_void + @gateway.expects(:ssl_request).returns(failed_void_response) + + response = @gateway.void('', @options) + assert_failure response + assert_nil response.authorization + assert response.test? + end + + def test_successful_verify_fields + card_on_file_fields = { + usage_indicator: '02', + indicator: '01', + scheduled_indicator: '02', + transaction_id: 'TXID00001293' + } + response = stub_comms do + @gateway.verify(@credit_card, @options.merge(card_on_file_fields)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transaction']['cardOnFile']['usageIndicator'], card_on_file_fields[:usage_indicator] + assert_equal request['transaction']['cardOnFile']['indicator'], card_on_file_fields[:indicator] + assert_equal request['transaction']['cardOnFile']['scheduledIndicator'], card_on_file_fields[:scheduled_indicator] + assert_equal request['transaction']['cardOnFile']['transactionId'], card_on_file_fields[:transaction_id] + assert_not_nil request['dateTime'] + assert !request['customer'].nil? && !request['customer'].empty? + assert_nil request['card']['entryMode'] + end.respond_with(successful_verify_response) + + assert_success response + end + + def test_successful_verify_with_stored_credential_framework + stored_credential_options = { + reason_type: 'recurring', + network_transaction_id: '123abcdefg' + } + stub_comms do + @gateway.verify(@credit_card, @options.merge({ stored_credential: stored_credential_options })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data)['transaction'] + assert_equal request['cardOnFile']['usageIndicator'], '02' + assert_equal request['cardOnFile']['indicator'], '01' + assert_equal request['cardOnFile']['scheduledIndicator'], '01' + assert_equal request['cardOnFile']['transactionId'], stored_credential_options[:network_transaction_id] + end.respond_with(successful_verify_response) + end + + def test_card_present_field + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['card']['present'], 'N' + end.respond_with(successful_purchase_response) + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge({ card_present: 'Y' })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['card']['present'], 'Y' + end.respond_with(successful_purchase_response) + end + + def test_successful_header_fields + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, _data, headers| + assert_equal headers['CompanyName'], 'Spreedly' + assert_equal headers['InterfaceVersion'], '1' + assert_equal headers['InterfaceName'], 'Spreedly' + end.respond_with(successful_purchase_response) + end + + def test_successful_time_zone_offset + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge!(merchant_time_zone: 'EST')) + end.check_request do |_endpoint, data, _headers| + assert_equal DateTime.parse(JSON.parse(data)['dateTime']).formatted_offset, Time.now.in_time_zone(@options[:merchant_time_zone]).formatted_offset + end.respond_with(successful_purchase_response) + end + + def test_support_scrub + assert @gateway.supports_scrubbing? + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_setup_access_token_should_rise_an_exception_under_unsuccessful_request + @gateway.expects(:ssl_post).returns(failed_auth_response) + + error = assert_raises(ActiveMerchant::OAuthResponseError) do + @gateway.setup_access_token + end + + assert_match(/Failed with AuthToken not valid ENGINE22CE/, error.message) + end + + def test_setup_access_token_should_successfully_extract_the_token_from_response + @gateway.expects(:ssl_post).returns(sucess_auth_response) + + assert_equal 'abc123', @gateway.setup_access_token + end + + private + + def response_result(response) + response.params['result'].first + end + + def pre_scrubbed + <<-PRE_SCRUBBED + opening connection to utgapi.shift4test.com:443... + opened + starting SSL for utgapi.shift4test.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256 + <- "POST /api/rest/v1/transactions/authorization HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nCompanyname: Spreedly\r\nAccesstoken: 4902FAD2-E88F-4A8D-98C2-EED2A73DBBE2\r\nInvoice: 1\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: utgapi.shift4test.com\r\nContent-Length: 498\r\n\r\n" + <- "{\"dateTime\":\"2022-06-09T14:03:36.413505000+14:03\",\"amount\":{\"total\":5.0,\"tax\":1.0},\"clerk\":{\"numericId\":24},\"transaction\":{\"invoice\":\"1\",\"purchaseCard\":{\"customerReference\":\"457\",\"destinationPostalCode\":\"89123\",\"productDescriptors\":[\"Potential\",\"Wrong\"]}},\"card\":{\"expirationDate\":\"0923\",\"number\":\"4000100011112224\",\"entryMode\":null,\"present\":null,\"securityCode\":{\"indicator\":1,\"value\":\"4444\"}},\"customer\":{\"addressLine1\":\"89 Main Street\",\"firstName\":\"XYZ\",\"lastName\":\"RON\",\"postalCode\":\"89000\"}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Connection: close\r\n" + -> "Content-Type: text/json; charset=ISO-8859-1\r\n" + -> "Content-Length: 1074\r\n" + -> "Date: Thu, 09 Jun 2022 09:03:40 GMT\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Frame-Options: deny\r\n" + -> "Content-Security-Policy: default-src 'none';base-uri 'none';frame-ancestors 'none';object-src 'none';sandbox;\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-Permitted-Cross-Domain-Policies: none\r\n" + -> "Referrer-Policy: no-referrer\r\n" + -> "X-Powered-By: Electricity\r\n" + -> "Expires: 0\r\n" + -> "Cache-Control: private, no-cache, no-store, max-age=0, no-transform\r\n" + -> "Server: DatasnapHTTPService/2011\r\n" + -> "\r\n" + reading 1074 bytes... + -> "" + -> "{\"result\":[{\"dateTime\":\"2022-06-09T14:03:36.000-07:00\",\"receiptColumns\":30,\"amount\":{\"tax\":1,\"total\":5},\"card\":{\"type\":\"VS\",\"entryMode\":\"M\",\"number\":\"XXXXXXXXXXXX2224\",\"present\":\"Y\",\"securityCode\":{\"result\":\"N\",\"valid\":\"N\"},\"token\":{\"value\":\"8042728003772224\"}},\"clerk\":{\"numericId\":24},\"customer\":{\"addressLine1\":\"89 Main Street\",\"firstName\":\"XYZ\",\"lastName\":\"RON\",\"postalCode\":\"89000\"},\"device\":{\"capability\":{\"magstripe\":\"Y\",\"manualEntry\":\"Y\"}},\"merchant\":{\"mid\":8504672,\"name\":\"Zippin - Retail\"},\"receipt\":[{\"key\":\"MaskedPAN\",\"printValue\":\"XXXXXXXXXXXX2224\"},{\"key\":\"CardEntryMode\",\"printName\":\"ENTRY METHOD\",\"printValue\":\"KEYED\"},{\"key\":\"SignatureRequired\",\"printValue\":\"N\"}],\"server\":{\"name\":\"UTGAPI05CE\"},\"transaction\":{\"authSource\":\"E\",\"avs\":{\"postalCodeVerified\":\"Y\",\"result\":\"Y\",\"streetVerified\":\"Y\",\"valid\":\"Y\"},\"invoice\":\"0000000001\",\"purchaseCard\":{\"customerReference\":\"457\",\"destinationPostalCode\":\"89123\",\"productDescriptors\":[\"Potential\",\"Wrong\"]},\"responseCode\":\"D\",\"saleFlag\":\"S\"},\"universalToken\":{\"value\":\"400010-2F1AA405-001AA4-000026B7-1766C44E9E8\"}}]}" + read 1074 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SCRUBBED + opening connection to utgapi.shift4test.com:443... + opened + starting SSL for utgapi.shift4test.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256 + <- "POST /api/rest/v1/transactions/authorization HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nCompanyname: Spreedly\r\nAccesstoken: 4902FAD2-E88F-4A8D-98C2-EED2A73DBBE2\r\nInvoice: 1\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: utgapi.shift4test.com\r\nContent-Length: 498\r\n\r\n" + <- "{\"dateTime\":\"2022-06-09T14:03:36.413505000+14:03\",\"amount\":{\"total\":5.0,\"tax\":1.0},\"clerk\":{\"numericId\":24},\"transaction\":{\"invoice\":\"1\",\"purchaseCard\":{\"customerReference\":\"457\",\"destinationPostalCode\":\"89123\",\"productDescriptors\":[\"Potential\",\"Wrong\"]}},\"card\":{\"expirationDate\":\"[FILTERED]",\"number\":\"[FILTERED]",\"entryMode\":null,\"present\":null,\"securityCode\":{\"indicator\":1,\"value\":\"[FILTERED]\"}},\"customer\":{\"addressLine1\":\"89 Main Street\",\"firstName\":\"[FILTERED]",\"lastName\":\"[FILTERED]",\"postalCode\":\"89000\"}}" + -> "HTTP/1.1 200 OK\r\n" + -> "Connection: close\r\n" + -> "Content-Type: text/json; charset=ISO-8859-1\r\n" + -> "Content-Length: 1074\r\n" + -> "Date: Thu, 09 Jun 2022 09:03:40 GMT\r\n" + -> "Pragma: no-cache\r\n" + -> "X-Frame-Options: deny\r\n" + -> "Content-Security-Policy: default-src 'none';base-uri 'none';frame-ancestors 'none';object-src 'none';sandbox;\r\n" + -> "Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "X-Content-Type-Options: nosniff\r\n" + -> "X-Permitted-Cross-Domain-Policies: none\r\n" + -> "Referrer-Policy: no-referrer\r\n" + -> "X-Powered-By: Electricity\r\n" + -> "Expires: 0\r\n" + -> "Cache-Control: private, no-cache, no-store, max-age=0, no-transform\r\n" + -> "Server: DatasnapHTTPService/2011\r\n" + -> "\r\n" + reading 1074 bytes... + -> "" + -> "{\"result\":[{\"dateTime\":\"2022-06-09T14:03:36.000-07:00\",\"receiptColumns\":30,\"amount\":{\"tax\":1,\"total\":5},\"card\":{\"type\":\"VS\",\"entryMode\":\"M\",\"number\":\"[FILTERED]",\"present\":\"Y\",\"securityCode\":{\"result\":\"N\",\"valid\":\"N\"},\"token\":{\"value\":\"8042728003772224\"}},\"clerk\":{\"numericId\":24},\"customer\":{\"addressLine1\":\"89 Main Street\",\"firstName\":\"[FILTERED]",\"lastName\":\"[FILTERED]",\"postalCode\":\"89000\"},\"device\":{\"capability\":{\"magstripe\":\"Y\",\"manualEntry\":\"Y\"}},\"merchant\":{\"mid\":8504672,\"name\":\"Zippin - Retail\"},\"receipt\":[{\"key\":\"MaskedPAN\",\"printValue\":\"XXXXXXXXXXXX2224\"},{\"key\":\"CardEntryMode\",\"printName\":\"ENTRY METHOD\",\"printValue\":\"KEYED\"},{\"key\":\"SignatureRequired\",\"printValue\":\"N\"}],\"server\":{\"name\":\"UTGAPI05CE\"},\"transaction\":{\"authSource\":\"E\",\"avs\":{\"postalCodeVerified\":\"Y\",\"result\":\"Y\",\"streetVerified\":\"Y\",\"valid\":\"Y\"},\"invoice\":\"0000000001\",\"purchaseCard\":{\"customerReference\":\"457\",\"destinationPostalCode\":\"89123\",\"productDescriptors\":[\"Potential\",\"Wrong\"]},\"responseCode\":\"D\",\"saleFlag\":\"S\"},\"universalToken\":{\"value\":\"400010-2F1AA405-001AA4-000026B7-1766C44E9E8\"}}]}" + read 1074 bytes + Conn close + POST_SCRUBBED + end + + def successful_purchase_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-02-09T05:11:54.000-08:00", + "receiptColumns": 30, + "amount": { + "total": 5 + }, + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX1111", + "present": "N", + "securityCode": { + "result": "N", + "valid": "N" + }, + "token": { + "value": "8042714004661111" + } + }, + "clerk": { + "numericId": 16 + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "mid": 8585812, + "name": "RealtimePOS - Retail" + }, + "receipt": [ + { + "key": "MaskedPAN", + "printValue": "XXXXXXXXXXXX1111" + }, + { + "key": "CardEntryMode", + "printName": "ENTRY METHOD", + "printValue": "KEYED" + }, + { + "key": "SignatureRequired", + "printValue": "N" + } + ], + "server": { + "name": "UTGAPI12CE" + }, + "transaction": { + "authSource": "E", + "invoice": "4666309473", + "purchaseCard": { + "customerReference": "1234567", + "destinationPostalCode": "89123", + "productDescriptors": [ + "Test" + ] + }, + "responseCode": "A", + "saleFlag": "S" + }, + "universalToken": { + "value": "444433-2D5C1A5C-001624-00001621-16BAAF4ACC6" + } + } + ] + } + RESPONSE + end + + def successful_store_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-06-27T13:06:07.000-07:00", + "receiptColumns": 30, + "card": { + "type": "VS", + "number": "XXXXXXXXXXXX2224", + "securityCode": {}, + "token": { + "value": "22243v5f0vkezpej" + } + }, + "merchant": { + "mid": 8628968 + }, + "server": { + "name": "UTGAPI11CE" + }, + "universalToken": { + "value": "400010-2F1AA405-001AA4-000026B7-1766C44E9E8" + } + } + ] + } + RESPONSE + end + + def successful_authorize_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-05-02T02:19:38.000-07:00", + "receiptColumns": 30, + "amount": { + "total": 5 + }, + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX1111", + "present": "N", + "securityCode": {}, + "token": { + "value": "8042677003331111" + } + }, + "clerk": { + "numericId": 24 + }, + "customer": { + "addressLine1": "89 Main Street", + "firstName": "XYZ", + "lastName": "RON", + "postalCode": "89000" + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "mid": 8504672 + }, + "receipt": [ + { + "key": "MaskedPAN", + "printValue": "XXXXXXXXXXXX1111" + }, + { + "key": "CardEntryMode", + "printName": "ENTRY METHOD", + "printValue": "KEYED" + }, + { + "key": "SignatureRequired", + "printValue": "Y" + } + ], + "server": { + "name": "UTGAPI12CE" + }, + "transaction": { + "authorizationCode": "OK168Z", + "authSource": "E", + "invoice": "3333333309", + "purchaseCard": { + "customerReference": "457", + "destinationPostalCode": "89123", + "productDescriptors": [ + "Potential", + "Wrong" + ] + }, + "responseCode": "A", + "saleFlag": "S" + }, + "universalToken": { + "value": "444433-2D5C1A5C-001624-00001621-16BAAF4ACC6" + } + } + ] + } + RESPONSE + end + + def successful_capture_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-05-08T01:18:22.000-07:00", + "receiptColumns": 30, + "amount": { + "total": 5 + }, + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX1111", + "present": "N", + "token": { + "value": "1111x19h4cryk231" + } + }, + "clerk": { + "numericId": 24 + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "mid": 8628968 + }, + "receipt": [ + { + "key": "MaskedPAN", + "printValue": "XXXXXXXXXXXX1111" + }, + { + "key": "CardEntryMode", + "printName": "ENTRY METHOD", + "printValue": "KEYED" + }, + { + "key": "SignatureRequired", + "printValue": "Y" + } + ], + "server": { + "name": "UTGAPI03CE" + }, + "transaction": { + "authorizationCode": "OK207Z", + "authSource": "E", + "invoice": "3333333309", + "responseCode": "A", + "saleFlag": "S" + }, + "universalToken": { + "value": "444433-2D5C1A5C-001624-00001621-16BAAF4ACC6" + } + } + ] + } + RESPONSE + end + + def successful_refund_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-02-09T05:11:54.000-08:00", + "receiptColumns": 30, + "amount": { + "total": 5 + }, + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX1111", + "present": "N", + "securityCode": { + "result": "N", + "valid": "N" + }, + "token": { + "value": "8042714004661111" + } + }, + "clerk": { + "numericId": 16 + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "mid": 8585812, + "name": "RealtimePOS - Retail" + }, + "receipt": [ + { + "key": "MaskedPAN", + "printValue": "XXXXXXXXXXXX1111" + }, + { + "key": "CardEntryMode", + "printName": "ENTRY METHOD", + "printValue": "KEYED" + }, + { + "key": "SignatureRequired", + "printValue": "N" + } + ], + "server": { + "name": "UTGAPI12CE" + }, + "transaction": { + "authSource": "E", + "invoice": "4666309473", + "purchaseCard": { + "customerReference": "1234567", + "destinationPostalCode": "89123", + "productDescriptors": [ + "Test" + ] + }, + "responseCode": "A", + "saleFlag": "S" + }, + "universalToken": { + "value": "444433-2D5C1A5C-001624-00001621-16BAAF4ACC6" + } + } + ] + } + RESPONSE + end + + def successful_void_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-05-16T14:59:54.000-07:00", + "receiptColumns": 30, + "amount": { + "total": 5 + }, + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX2224", + "token": { + "value": "2224kz7vybyv1gs3" + } + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "mid": 8628968 + }, + "receipt": [ + { + "key": "TransactionResponse", + "printName": "Response", + "printValue": "SALE CORRECTION" + }, + { + "key": "MaskedPAN", + "printValue": "XXXXXXXXXXXX2224" + }, + { + "key": "CardEntryMode", + "printName": "ENTRY METHOD", + "printValue": "KEYED" + }, + { + "key": "SignatureRequired", + "printValue": "N" + } + ], + "server": { + "name": "UTGAPI07CE" + }, + "transaction": { + "authSource": "E", + "invoice": "0000000001", + "responseCode": "A", + "saleFlag": "S" + } + } + ] + } + RESPONSE + end + + def successful_verify_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-09-16T01:40:51.000-07:00", + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX2224", + "present": "N", + "securityCode": { + "result": "M", + "valid": "Y" + }, + "token": { + "value": "2224xzsetmjksx13" + } + }, + "customer": { + "firstName": "John", + "lastName": "Smith" + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "name": "Spreedly - ECom" + }, + "server": { + "name": "UTGAPI12CE" + }, + "transaction": { + "authorizationCode": "OK684Z", + "authSource": "E", + "responseCode": "A", + "saleFlag": "S" + }, + "universalToken": { + "value": "400010-2F1AA405-001AA4-000026B7-1766C44E9E8" + } + } + ] + } + RESPONSE + end + + def failed_authorize_response + <<-RESPONSE + { + "result": [ + { + "error": { + "longText": "GTV Msg: ERROR{0} 20018: no default category found, UC, Mod10=N TOKEN01CE ENGINE29CE", + "primaryCode": 9100, + "shortText": "SYSTEM ERROR" + }, + "server": { + "name": "UTGAPI12CE" + } + } + ] + } + RESPONSE + end + + def failed_purchase_response + <<-RESPONSE + { + "result": [ + { + "error": { + "longText": "Token contains invalid characters UTGAPI08CE", + "primaryCode": 9864, + "shortText": "Invalid Token" + }, + "server": { + "name": "UTGAPI08CE" + } + } + ] + } + RESPONSE + end + + def failed_capture_response + <<-RESPONSE + { + "result": [ + { + "error": { + "longText": "INTERNET FAILURE: Timeout waiting for response across the Internet UTGAPI05CE", + "primaryCode": 9961, + "shortText": "INTERNET FAILURE" + }, + "server": { + "name": "UTGAPI05CE" + } + } + ] + } + RESPONSE + end + + def failed_refund_response + <<-RESPONSE + { + "result": [ + { + "error": { + "longText": "record not posted ENGINE21CE", + "primaryCode": 9844, + "shortText": "I/O ERROR" + }, + "server": { + "name": "UTGAPI05CE" + } + } + ] + } + RESPONSE + end + + def failed_void_response + <<-RESPONSE + { + "result": [ + { + "error": { + "longText": "Invoice Not Found 00000000kl 0008628968 ENGINE29CE", + "primaryCode": 9815, + "shortText": "NO INV" + }, + "server": { + "name": "UTGAPI13CE" + } + } + ] + } + RESPONSE + end + + def successful_access_token_response + <<-RESPONSE + { + "result": [ + { + "dateTime": "2022-06-22T15:27:51.000-07:00", + "receiptColumns": 30, + "credential": { + "accessToken": "3F6A334E-01E5-4EDB-B4CE-0B1BEFC13518" + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "server": { + "name": "UTGAPI09CE" + } + } + ] + } + RESPONSE + end + + def failed_auth_response + <<-RESPONSE + { + "result": [ + { + "error": { + "longText": "AuthToken not valid ENGINE22CE", + "primaryCode": 9862, + "secondaryCode": 4, + "shortText ": "AuthToken" + }, + "server": { + "name": "UTGAPI03CE" + } + } + ] + } + RESPONSE + end + + def failed_auth_response_no_message + <<-RESPONSE + { + "result": [ + { + "error": { + "secondaryCode": 4, + "shortText ": "AuthToken" + }, + "server": { + "name": "UTGAPI03CE" + } + } + ] + } + RESPONSE + end + + def sucess_auth_response + <<-RESPONSE + { + "result": [ + { + "credential": { + "accessToken": "abc123" + } + } + ] + } + RESPONSE + end +end diff --git a/test/unit/gateways/shift4_v2_test.rb b/test/unit/gateways/shift4_v2_test.rb new file mode 100644 index 00000000000..7bc4747aff3 --- /dev/null +++ b/test/unit/gateways/shift4_v2_test.rb @@ -0,0 +1,91 @@ +require 'test_helper' +require_relative 'securion_pay_test' + +class Shift4V2Test < SecurionPayTest + include CommStub + + def setup + super + @gateway = Shift4V2Gateway.new( + secret_key: 'pr_test_random_key' + ) + end + + def test_invalid_raw_response + @gateway.expects(:ssl_request).returns(invalid_json_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match(/^Invalid response received from the Shift4 V2 API/, response.message) + end + + def test_amount_gets_upcased_if_needed + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_equal 'USD', CGI.parse(data)['currency'].first + end.respond_with(successful_purchase_response) + end + + private + + def pre_scrubbed + <<-PRE_SCRUBBED + opening connection to api.shift4.com:443... + opened + starting SSL for api.shift4.com:443... + SSL established + <- "POST /charges HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic cHJfdGVzdF9xWk40VlZJS0N5U2ZDZVhDQm9ITzlEQmU6\r\nUser-Agent: SecurionPay/v1 ActiveMerchantBindings/1.47.0\r\nAccept-Encoding: gzip;q=0,deflate;q=0.6\r\nAccept: */*\r\nConnection: close\r\nHost: api.shift4.com\r\nContent-Length: 214\r\n\r\n" + <- "amount=2000¤cy=usd&card[number]=4242424242424242&card[expMonth]=9&card[expYear]=2016&card[cvc]=123&card[cardholderName]=Longbob+Longsen&description=ActiveMerchant+test+charge&metadata[email]=foo%40example.com" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: cloudflare-nginx\r\n" + -> "Date: Fri, 12 Jun 2015 21:36:39 GMT\r\n" + -> "Content-Type: application/json;charset=UTF-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: __cfduid=d5da73266c61acce6307176d45e2672b41434144998; expires=Sat, 11-Jun-16 21:36:38 GMT; path=/; domain=.securionpay.com; HttpOnly\r\n" + -> "CF-RAY: 1f58b1414ca00af6-WAW\r\n" + -> "\r\n" + -> "1f4\r\n" + reading 500 bytes... + -> "{\"id\":\"char_TOnen0ZcDMYzECNS4fItK9P4\",\"created\":1434144998,\"objectType\":\"charge\",\"amount\":2000,\"currency\":\"USD\",\"description\":\"ActiveMerchant test charge\",\"card\":{\"id\":\"card_yJ4JNcp6P4sG8UrtZ62VWb5e\",\"created\":1434144998,\"objectType\":\"card\",\"first6\":\"424242\",\"last4\":\"4242\",\"fingerprint\":\"ecAKhFD1dmDAMKD9\",\"expMonth\":\"9\",\"expYear\":\"2016\",\"cardholderName\":\"Longbob Longsen\",\"brand\":\"Visa\",\"type\":\"Credit Card\"},\"captured\":true,\"refunded\":false,\"disputed\":false,\"metadata\":{\"email\":\"foo@example.com\"}}" + read 500 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SCRUBBED + opening connection to api.shift4.com:443... + opened + starting SSL for api.shift4.com:443... + SSL established + <- "POST /charges HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic [FILTERED]\r\nUser-Agent: SecurionPay/v1 ActiveMerchantBindings/1.47.0\r\nAccept-Encoding: gzip;q=0,deflate;q=0.6\r\nAccept: */*\r\nConnection: close\r\nHost: api.shift4.com\r\nContent-Length: 214\r\n\r\n" + <- "amount=2000¤cy=usd&card[number]=[FILTERED]&card[expMonth]=[FILTERED]&card[expYear]=[FILTERED]&card[cvc]=[FILTERED]&card[cardholderName]=[FILTERED]&description=ActiveMerchant+test+charge&metadata[email]=foo%40example.com" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: cloudflare-nginx\r\n" + -> "Date: Fri, 12 Jun 2015 21:36:39 GMT\r\n" + -> "Content-Type: application/json;charset=UTF-8\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: __cfduid=d5da73266c61acce6307176d45e2672b41434144998; expires=Sat, 11-Jun-16 21:36:38 GMT; path=/; domain=.securionpay.com; HttpOnly\r\n" + -> "CF-RAY: 1f58b1414ca00af6-WAW\r\n" + -> "\r\n" + -> "1f4\r\n" + reading 500 bytes... + -> "{\"id\":\"char_TOnen0ZcDMYzECNS4fItK9P4\",\"created\":1434144998,\"objectType\":\"charge\",\"amount\":2000,\"currency\":\"USD\",\"description\":\"ActiveMerchant test charge\",\"card\":{\"id\":\"card_yJ4JNcp6P4sG8UrtZ62VWb5e\",\"created\":1434144998,\"objectType\":\"card\",\"first6\":\"424242\",\"last4\":\"4242\",\"fingerprint\":\"ecAKhFD1dmDAMKD9\",\"expMonth\":\"9\",\"expYear\":\"2016\",\"cardholderName\":\"Longbob Longsen\",\"brand\":\"Visa\",\"type\":\"Credit Card\"},\"captured\":true,\"refunded\":false,\"disputed\":false,\"metadata\":{\"email\":\"foo@example.com\"}}" + read 500 bytes + reading 2 bytes... + -> "\r\n" + read 2 bytes + -> "0\r\n" + -> "\r\n" + Conn close + POST_SCRUBBED + end +end diff --git a/test/unit/gateways/simetrik_test.rb b/test/unit/gateways/simetrik_test.rb index 899123134b2..c120b2e99fe 100644 --- a/test/unit/gateways/simetrik_test.rb +++ b/test/unit/gateways/simetrik_test.rb @@ -56,7 +56,7 @@ def setup installments: 1 }, currency: 'USD', - vat: 19, + vat: 190, three_ds_fields: { version: '2.1.0', eci: '02', @@ -92,7 +92,7 @@ def setup "amount": { "total_amount": 10.0, "currency": 'USD', - "vat": 19 + "vat": 1.9 } }, "payment_method": { @@ -170,6 +170,26 @@ def test_success_purchase_with_billing_address assert response.test? end + def test_success_purchase_with_shipping_address + expected_body = JSON.parse(@authorize_capture_expected_body.dup) + expected_body['forward_payload']['order']['shipping_address'] = address + + @gateway.expects(:ssl_request).returns(successful_purchase_response_body) + + options = @authorize_capture_options.clone() + options[:shipping_address] = address + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_instance_of Response, response + + assert_equal response.message, 'successful charge' + assert_equal response.error_code, nil, 'Should expected error code equal to nil ' + assert_equal response.avs_result['code'], 'G' + assert_equal response.cvv_result['code'], 'P' + assert response.test? + end + def test_failed_purchase @gateway.expects(:ssl_request).returns(failed_purchase_response_body) @@ -232,7 +252,7 @@ def test_failed_capture @gateway.expects(:ssl_request).returns(failed_capture_response_body) response = @gateway.capture(@amount, 'SI-226', { - vat: 19, + vat: 190, currency: 'USD', token_acquirer: @token_acquirer, trace_id: @trace_id diff --git a/test/unit/gateways/stripe_payment_intents_test.rb b/test/unit/gateways/stripe_payment_intents_test.rb index 3c1044625d5..175b6d16211 100644 --- a/test/unit/gateways/stripe_payment_intents_test.rb +++ b/test/unit/gateways/stripe_payment_intents_test.rb @@ -4,17 +4,19 @@ class StripePaymentIntentsTest < Test::Unit::TestCase include CommStub def setup - @gateway = StripePaymentIntentsGateway.new(login: 'login') + @gateway = StripePaymentIntentsGateway.new(login: 'sk_test_login') @credit_card = credit_card() @threeds_2_card = credit_card('4000000000003220') @visa_token = 'pm_card_visa' @three_ds_authentication_required_setup_for_off_session = 'pm_card_authenticationRequiredSetupForOffSession' - @three_ds_off_session_credit_card = credit_card('4000002500003155', + @three_ds_off_session_credit_card = credit_card( + '4000002500003155', verification_value: '737', month: 10, - year: 2022) + year: 2022 + ) @amount = 2020 @update_amount = 2050 @@ -31,9 +33,7 @@ def setup brand: 'visa', eci: '05', month: '09', - year: '2030', - first_name: 'Longbob', - last_name: 'Longsen' + year: '2030' ) @apple_pay = network_tokenization_credit_card( @@ -47,6 +47,18 @@ def setup first_name: 'Longbob', last_name: 'Longsen' ) + + @network_token_credit_card = network_tokenization_credit_card( + '4000056655665556', + verification_value: '123', + payment_cryptogram: 'dGVzdGNyeXB0b2dyYW1YWFhYWFhYWFhYWFg9PQ==', + source: :network_token, + brand: 'visa', + month: '09', + year: '2030', + first_name: 'Longbob', + last_name: 'Longsen' + ) end def test_successful_create_and_confirm_intent @@ -78,6 +90,19 @@ def test_successful_create_and_capture_intent assert_equal 'Payment complete.', capture.params.dig('charges', 'data')[0].dig('outcome', 'seller_message') end + def test_successful_create_and_capture_intent_with_network_token + options = @options.merge(capture_method: 'manual', confirm: true) + @gateway.expects(:ssl_request).twice.returns(successful_create_intent_manual_capture_response_with_network_token_fields, successful_manual_capture_of_payment_intent_response_with_network_token_fields) + assert create = @gateway.create_intent(@amount, @network_token_credit_card, options) + assert_success create + assert_equal 'requires_capture', create.params['status'] + + assert capture = @gateway.capture(@amount, create.params['id'], options) + assert_success capture + assert_equal 'succeeded', capture.params['status'] + assert_equal 'Payment complete.', capture.params.dig('charges', 'data')[0].dig('outcome', 'seller_message') + end + def test_successful_create_and_update_intent @gateway.expects(:ssl_request).twice.returns(successful_create_intent_response, successful_update_intent_response) assert create = @gateway.create_intent(@amount, @visa_token, @options.merge(capture_method: 'manual')) @@ -259,6 +284,28 @@ def test_failed_payment_methods_post assert_equal 'invalid_request_error', create.params.dig('error', 'type') end + def test_invalid_test_login_for_sk_key + gateway = StripePaymentIntentsGateway.new(login: 'sk_live_3422') + assert response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match 'Invalid API Key provided', response.message + end + + def test_invalid_test_login_for_rk_key + gateway = StripePaymentIntentsGateway.new(login: 'rk_live_3422') + assert response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match 'Invalid API Key provided', response.message + end + + def test_successful_purchase + gateway = StripePaymentIntentsGateway.new(login: '3422e230423s') + + stub_comms(gateway, :ssl_request) do + gateway.purchase(@amount, @credit_card, @options) + end.respond_with(successful_create_intent_response) + end + def test_failed_error_on_requires_action @gateway.expects(:ssl_request).returns(failed_with_set_error_on_requires_action_response) @@ -291,6 +338,90 @@ def test_successful_verify assert_equal 'succeeded', verify.params['status'] end + def test_successful_purchase_with_level3_data + @options[:merchant_reference] = 123 + @options[:customer_reference] = 456 + @options[:shipping_address_zip] = 98765 + @options[:shipping_from_zip] = 54321 + @options[:shipping_amount] = 40 + @options[:line_items] = [ + { + 'product_code' => 1234, + 'product_description' => 'An item', + 'unit_cost' => 60, + 'quantity' => 7, + 'tax_amount' => 0 + }, + { + 'product_code' => 999, + 'tax_amount' => 888 + } + ] + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('level3[merchant_reference]=123', data) + assert_match('level3[customer_reference]=456', data) + assert_match('level3[shipping_address_zip]=98765', data) + assert_match('level3[shipping_amount]=40', data) + assert_match('level3[shipping_from_zip]=54321', data) + assert_match('level3[line_items][0][product_description]=An+item', data) + assert_match('level3[line_items][1][product_code]=999', data) + end.respond_with(successful_create_intent_response) + end + + def test_succesful_purchase_with_stored_credentials_without_sending_ntid + [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + network_transaction_id = '1098510912210968' + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, card_to_use, { + currency: 'USD', + execute_threed: true, + confirm: true, + off_session: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'cardholder', + reason_type: 'installment', + initial_transaction: true, + network_transaction_id: network_transaction_id, # TEST env seems happy with any value :/ + ds_transaction_id: 'null' # this is optional and can be null if not available. + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_no_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=}, data) + assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[ds_transaction_id\]=null}, data) + end.respond_with(successful_create_intent_response) + end + end + + def test_succesful_purchase_with_ntid_when_off_session + # don't send NTID if setup_future_usage == off_session + [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| + network_transaction_id = '1098510912210968' + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, card_to_use, { + currency: 'USD', + execute_threed: true, + confirm: true, + off_session: true, + setup_future_usage: 'off_session', + stored_credential: { + initiator: 'cardholder', + reason_type: 'installment', + initial_transaction: true, + network_transaction_id: network_transaction_id, # TEST env seems happy with any value :/ + ds_transaction_id: 'null' # this is optional and can be null if not available. + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_no_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=}, data) + assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[ds_transaction_id\]=null}, data) + end.respond_with(successful_create_intent_response) + end + end + def test_succesful_purchase_with_stored_credentials [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| network_transaction_id = '1098510912210968' @@ -322,7 +453,7 @@ def test_succesful_purchase_with_stored_credentials_without_optional_ds_transact confirm: true, off_session: true, stored_credential: { - network_transaction_id: network_transaction_id, # TEST env seems happy with any value :/ + network_transaction_id: network_transaction_id # TEST env seems happy with any value :/ } }) end.check_request do |_method, _endpoint, data, _headers| @@ -358,6 +489,14 @@ def test_sends_network_transaction_id_separate_from_stored_creds end.respond_with(successful_create_intent_response) end + def test_sends_expand_balance_transaction + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('expand[0]=charges.data.balance_transaction', data) + end.respond_with(successful_create_intent_response) + end + def test_purchase_with_google_pay options = { currency: 'GBP' @@ -370,6 +509,46 @@ def test_purchase_with_google_pay end.respond_with(successful_create_intent_response) end + def test_purchase_with_google_pay_with_billing_address + options = { + currency: 'GBP', + billing_address: address + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @google_pay, options) + end.check_request do |_method, endpoint, data, _headers| + if %r{/tokens}.match?(endpoint) + assert_match('card[name]=Jim+Smith', data) + assert_match('card[tokenization_method]=android_pay', data) + assert_match('card[address_line1]=456+My+Street', data) + end + assert_match('wallet[type]=google_pay', data) if %r{/wallet}.match?(endpoint) + assert_match('payment_method=pi_', data) if %r{/payment_intents}.match?(endpoint) + end.respond_with(successful_create_intent_response_with_google_pay_and_billing_address) + end + + def test_purchase_with_network_token_card + options = { + currency: 'USD', + last_4: '4242' + } + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @network_token_credit_card, options) + end.check_request do |_method, endpoint, data, _headers| + assert_match(%r{/payment_intents}, endpoint) + assert_match('confirm=true', data) + assert_match('payment_method_data[type]=card', data) + assert_match('[card][exp_month]=9', data) + assert_match('[card][exp_year]=2030', data) + assert_match('[card][last4]=4242', data) + assert_match('[card][network_token][number]=4000056655665556', data) + assert_match("[card][network_token][cryptogram]=#{URI.encode_www_form_component('dGVzdGNyeXB0b2dyYW1YWFhYWFhYWFhYWFg9PQ==')}", data) + assert_match('[card][network_token][exp_month]=9', data) + assert_match('[card][network_token][exp_year]=2030', data) + end.respond_with(successful_create_intent_response_with_network_token_fields) + end + def test_purchase_with_shipping_options options = { currency: 'GBP', @@ -382,7 +561,8 @@ def test_purchase_with_shipping_options address1: 'block C', address2: 'street 48', zip: '22400', - state: 'California' + state: 'California', + email: 'test@email.com' } } stub_comms(@gateway, :ssl_request) do @@ -396,13 +576,13 @@ def test_purchase_with_shipping_options assert_match('shipping[address][state]=California', data) assert_match('shipping[name]=John+Adam', data) assert_match('shipping[phone]=%2B0018313818368', data) + assert_no_match(/shipping[email]/, data) end.respond_with(successful_create_intent_response) end def test_purchase_with_shipping_carrier_and_tracking_number options = { currency: 'GBP', - customer: @customer, shipping_address: { name: 'John Adam', address1: 'block C' @@ -410,6 +590,7 @@ def test_purchase_with_shipping_carrier_and_tracking_number shipping_tracking_number: 'TXNABC123', shipping_carrier: 'FEDEX' } + options[:customer] = @customer if defined?(@customer) stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @visa_token, options) end.check_request do |_method, _endpoint, data, _headers| @@ -432,6 +613,20 @@ def test_authorize_with_apple_pay end.respond_with(successful_create_intent_response) end + def test_authorize_with_apple_pay_with_billing_address + options = { + currency: 'GBP', + billing_address: address + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @apple_pay, options) + end.check_request do |_method, endpoint, data, _headers| + assert_match('card[tokenization_method]=apple_pay', data) if %r{/tokens}.match?(endpoint) + assert_match('card[address_line1]=456+My+Street', data) if %r{/tokens}.match?(endpoint) + assert_match('payment_method=pi_', data) if %r{/payment_intents}.match?(endpoint) + end.respond_with(successful_create_intent_response_with_apple_pay_and_billing_address) + end + def test_stored_credentials_does_not_override_ntid_field network_transaction_id = '1098510912210968' sc_network_transaction_id = '1078784111114777' @@ -590,6 +785,108 @@ def test_scrub_filter_token assert_equal @gateway.scrub(pre_scrubbed), scrubbed end + def test_succesful_purchase_with_initial_cit_unscheduled + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token, { + currency: 'USD', + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'unscheduled' + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_options[card][stored_credential_transaction_type]=setup_off_session_unscheduled', data) + end.respond_with(successful_create_intent_response) + end + + def test_succesful_purchase_with_initial_cit_recurring + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token, { + currency: 'USD', + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'recurring' + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_options[card][stored_credential_transaction_type]=setup_off_session_recurring', data) + end.respond_with(successful_create_intent_response) + end + + def test_succesful_purchase_with_initial_cit_installment + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token, { + currency: 'USD', + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'installment' + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_options[card][stored_credential_transaction_type]=setup_on_session', data) + end.respond_with(successful_create_intent_response) + end + + def test_succesful_purchase_with_subsequent_cit + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token, { + currency: 'USD', + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initial_transaction: false, + initiator: 'cardholder', + reason_type: 'installment' + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_options[card][stored_credential_transaction_type]=stored_on_session', data) + end.respond_with(successful_create_intent_response) + end + + def test_succesful_purchase_with_mit_recurring + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token, { + currency: 'USD', + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'recurring' + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_options[card][stored_credential_transaction_type]=stored_off_session_recurring', data) + end.respond_with(successful_create_intent_response) + end + + def test_succesful_purchase_with_mit_unscheduled + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token, { + currency: 'USD', + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'unscheduled' + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_options[card][stored_credential_transaction_type]=stored_off_session_unscheduled', data) + end.respond_with(successful_create_intent_response) + end + private def successful_setup_purchase @@ -662,6 +959,546 @@ def successful_create_intent_response RESPONSE end + def successful_create_intent_response_with_network_token_fields + <<~RESPONSE + { + "id": "pi_3NfRruAWOtgoysog1FxgDwtf", + "object": "payment_intent", + "amount": 2000, + "amount_capturable": 0, + "amount_details": { + "tip": { + } + }, + "amount_received": 2000, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [ + { + "id": "ch_3NfRruAWOtgoysog1ptwVNHx", + "object": "charge", + "amount": 2000, + "amount_captured": 2000, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_3NfRruAWOtgoysog1mtFHzZr", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": "Longbob Longsen", + "phone": null + }, + "calculated_statement_descriptor": "SPREEDLY", + "captured": true, + "created": 1692123686, + "currency": "usd", + "customer": null, + "description": null, + "destination": null, + "dispute": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": null, + "failure_message": null, + "fraud_details": { + }, + "invoice": null, + "livemode": false, + "metadata": { + }, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 34, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_3NfRruAWOtgoysog1FxgDwtf", + "payment_method": "pm_1NfRruAWOtgoysogjdx336vt", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "ds_transaction_id": null, + "exp_month": 9, + "exp_year": 2030, + "fingerprint": null, + "funding": "debit", + "installments": null, + "last4": "4242", + "mandate": null, + "moto": null, + "network": "visa", + "network_token": { + "exp_month": 9, + "exp_year": 2030, + "fingerprint": "OdTRtGskBulROtqa", + "last4": "5556", + "used": false + }, + "network_transaction_id": "791008482116711", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xNjBEWDZBV090Z295c29nKKeE76YGMgbjse9I0TM6LBZ6z9Y1XXMETb-LDQ5oyLVXQhIMltBU0qwDkNKpNvrIGvXOhYmhorDkkE36", + "refunded": false, + "refunds": { + "object": "list", + "data": [ + ], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_3NfRruAWOtgoysog1ptwVNHx/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_3NfRruAWOtgoysog1FxgDwtf" + }, + "client_secret": "pi_3NfRruAWOtgoysog1FxgDwtf_secret_f4ke", + "confirmation_method": "automatic", + "created": 1692123686, + "currency": "usd", + "customer": null, + "description": null, + "invoice": null, + "last_payment_error": null, + "latest_charge": "ch_3NfRruAWOtgoysog1ptwVNHx", + "level3": null, + "livemode": false, + "metadata": { + }, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_1NfRruAWOtgoysogjdx336vt", + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + RESPONSE + end + + def successful_create_intent_manual_capture_response_with_network_token_fields + <<~RESPONSE + { + "id": "pi_3NfTpgAWOtgoysog1SqST5dL", + "object": "payment_intent", + "amount": 2000, + "amount_capturable": 2000, + "amount_details": { + "tip": { + } + }, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "manual", + "charges": { + "object": "list", + "data": [ + { + "id": "ch_3NfTpgAWOtgoysog1ZcuSdwZ", + "object": "charge", + "amount": 2000, + "amount_captured": 0, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": null, + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": "Longbob Longsen", + "phone": null + }, + "calculated_statement_descriptor": "SPREEDLY", + "captured": false, + "created": 1692131237, + "currency": "gbp", + "customer": "cus_OSOcijtQkDdBbF", + "description": null, + "destination": null, + "dispute": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": null, + "failure_message": null, + "fraud_details": { + }, + "invoice": null, + "livemode": false, + "metadata": { + }, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 24, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_3NfTpgAWOtgoysog1SqST5dL", + "payment_method": "pm_1NfTpgAWOtgoysogHnl1rNCf", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "ds_transaction_id": null, + "exp_month": 9, + "exp_year": 2030, + "fingerprint": null, + "funding": "debit", + "installments": null, + "last4": "4242", + "mandate": null, + "moto": null, + "network": "visa", + "network_token": { + "exp_month": 9, + "exp_year": 2030, + "fingerprint": "OdTRtGskBulROtqa", + "last4": "5556", + "used": false + }, + "network_transaction_id": "791008482116711", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xNjBEWDZBV090Z295c29nKKW_76YGMgZFk46uT_Y6LBZ51LZOrwdCQ0w176ShWIhNs2CXEh-L6A9pDYW33I_z6C6SenKNrWasw9Ie", + "refunded": false, + "refunds": { + "object": "list", + "data": [ + ], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_3NfTpgAWOtgoysog1ZcuSdwZ/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_3NfTpgAWOtgoysog1SqST5dL" + }, + "client_secret": "pi_3NfRruAWOtgoysog1FxgDwtf_secret_f4ke", + "confirmation_method": "manual", + "created": 1692131236, + "currency": "gbp", + "customer": "cus_OSOcijtQkDdBbF", + "description": null, + "invoice": null, + "last_payment_error": null, + "latest_charge": "ch_3NfTpgAWOtgoysog1ZcuSdwZ", + "level3": null, + "livemode": false, + "metadata": { + }, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_1NfTpgAWOtgoysogHnl1rNCf", + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "requires_capture", + "transfer_data": null, + "transfer_group": null + } + RESPONSE + end + + def successful_manual_capture_of_payment_intent_response_with_network_token_fields + <<-RESPONSE + { + "id": "pi_3NfTpgAWOtgoysog1SqST5dL", + "object": "payment_intent", + "amount": 2000, + "amount_capturable": 0, + "amount_details": { + "tip": { + } + }, + "amount_received": 2000, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "manual", + "charges": { + "object": "list", + "data": [ + { + "id": "ch_3NfTpgAWOtgoysog1ZcuSdwZ", + "object": "charge", + "amount": 2000, + "amount_captured": 2000, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_3NfTpgAWOtgoysog1ZTZXCvO", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": "Longbob Longsen", + "phone": null + }, + "calculated_statement_descriptor": "SPREEDLY", + "captured": true, + "created": 1692131237, + "currency": "gbp", + "customer": "cus_OSOcijtQkDdBbF", + "description": null, + "destination": null, + "dispute": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": null, + "failure_message": null, + "fraud_details": { + }, + "invoice": null, + "livemode": false, + "metadata": { + }, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 24, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_3NfTpgAWOtgoysog1SqST5dL", + "payment_method": "pm_1NfTpgAWOtgoysogHnl1rNCf", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "ds_transaction_id": null, + "exp_month": 9, + "exp_year": 2030, + "fingerprint": null, + "funding": "debit", + "installments": null, + "last4": "4242", + "mandate": null, + "moto": null, + "network": "visa", + "network_token": { + "exp_month": 9, + "exp_year": 2030, + "fingerprint": "OdTRtGskBulROtqa", + "last4": "5556", + "used": false + }, + "network_transaction_id": "791008482116711", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xNjBEWDZBV090Z295c29nKKa_76YGMgZZ4Fl_Etg6LBYGcD6D2xFTlgp69zLDZz1ZToBrKKjxhRCpYcnLWInSmJZHcjcBdrhyAKGv", + "refunded": false, + "refunds": { + "object": "list", + "data": [ + ], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_3NfTpgAWOtgoysog1ZcuSdwZ/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_3NfTpgAWOtgoysog1SqST5dL" + }, + "client_secret": "pi_3NfRruAWOtgoysog1FxgDwtf_secret_f4ke", + "confirmation_method": "manual", + "created": 1692131236, + "currency": "gbp", + "customer": "cus_OSOcijtQkDdBbF", + "description": null, + "invoice": null, + "last_payment_error": null, + "latest_charge": "ch_3NfTpgAWOtgoysog1ZcuSdwZ", + "level3": null, + "livemode": false, + "metadata": { + }, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_1NfTpgAWOtgoysogHnl1rNCf", + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + RESPONSE + end + + def successful_create_intent_response_with_apple_pay_and_billing_address + <<-RESPONSE + {"id"=>"pi_3N0mqdAWOtgoysog1IQeiLiz", "object"=>"payment_intent", "amount"=>2000, "amount_capturable"=>0, "amount_details"=>{"tip"=>{}}, "amount_received"=>2000, "application"=>nil, "application_fee_amount"=>nil, "automatic_payment_methods"=>nil, "canceled_at"=>nil, "cancellation_reason"=>nil, "capture_method"=>"automatic", "charges"=>{"object"=>"list", "data"=>[{"id"=>"ch_3N0mqdAWOtgoysog1HddFSKg", "object"=>"charge", "amount"=>2000, "amount_captured"=>2000, "amount_refunded"=>0, "application"=>nil, "application_fee"=>nil, "application_fee_amount"=>nil, "balance_transaction"=>"txn_3N0mqdAWOtgoysog1EpiFDCD", "billing_details"=>{"address"=>{"city"=>"Ottawa", "country"=>"CA", "line1"=>"456 My Street", "line2"=>"Apt 1", "postal_code"=>"K1C2N6", "state"=>"ON"}, "email"=>nil, "name"=>nil, "phone"=>nil}, "calculated_statement_descriptor"=>"SPREEDLY", "captured"=>true, "created"=>1682432883, "currency"=>"gbp", "customer"=>nil, "description"=>nil, "destination"=>nil, "dispute"=>nil, "disputed"=>false, "failure_balance_transaction"=>nil, "failure_code"=>nil, "failure_message"=>nil, "fraud_details"=>{}, "invoice"=>nil, "livemode"=>false, "metadata"=>{}, "on_behalf_of"=>nil, "order"=>nil, "outcome"=>{"network_status"=>"approved_by_network", "reason"=>nil, "risk_level"=>"normal", "risk_score"=>15, "seller_message"=>"Payment complete.", "type"=>"authorized"}, "paid"=>true, "payment_intent"=>"pi_3N0mqdAWOtgoysog1IQeiLiz", "payment_method"=>"pm_1N0mqdAWOtgoysogloANIhUF", "payment_method_details"=>{"card"=>{"brand"=>"visa", "checks"=>{"address_line1_check"=>"pass", "address_postal_code_check"=>"pass", "cvc_check"=>nil}, "country"=>"US", "ds_transaction_id"=>nil, "exp_month"=>9, "exp_year"=>2030, "fingerprint"=>"hfaVNMiXc0dYSiC5", "funding"=>"credit", "installments"=>nil, "last4"=>"0000", "mandate"=>nil, "moto"=>nil, "network"=>"visa", "network_token"=>{"used"=>false}, "network_transaction_id"=>"104102978678771", "three_d_secure"=>nil, "wallet"=>{"apple_pay"=>{"type"=>"apple_pay"}, "dynamic_last4"=>"4242", "type"=>"apple_pay"}}, "type"=>"card"}, "receipt_email"=>nil, "receipt_number"=>nil, "receipt_url"=>"https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xNjBEWDZBV090Z295c29nKPTGn6IGMgZMGrHHLa46LBY0n2_9_Yar0wPTNukle4t28eKG0ZDZnxGYr6GyKn8VsKIEVjU4NkW8NHTL", "refunded"=>false, "refunds"=>{"object"=>"list", "data"=>[], "has_more"=>false, "total_count"=>0, "url"=>"/v1/charges/ch_3N0mqdAWOtgoysog1HddFSKg/refunds"}, "review"=>nil, "shipping"=>nil, "source"=>nil, "source_transfer"=>nil, "statement_descriptor"=>nil, "statement_descriptor_suffix"=>nil, "status"=>"succeeded", "transfer_data"=>nil, "transfer_group"=>nil}], "has_more"=>false, "total_count"=>1, "url"=>"/v1/charges?payment_intent=pi_3N0mqdAWOtgoysog1IQeiLiz"}, "client_secret"=>"pi_3N0mqdAWOtgoysog1IQeiLiz_secret_laDLUM6rVleLRqz0nMus9HktB", "confirmation_method"=>"automatic", "created"=>1682432883, "currency"=>"gbp", "customer"=>nil, "description"=>nil, "invoice"=>nil, "last_payment_error"=>nil, "latest_charge"=>"ch_3N0mqdAWOtgoysog1HddFSKg", "level3"=>nil, "livemode"=>false, "metadata"=>{}, "next_action"=>nil, "on_behalf_of"=>nil, "payment_method"=>"pm_1N0mqdAWOtgoysogloANIhUF", "payment_method_options"=>{"card"=>{"installments"=>nil, "mandate_options"=>nil, "network"=>nil, "request_three_d_secure"=>"automatic"}}, "payment_method_types"=>["card"], "processing"=>nil, "receipt_email"=>nil, "review"=>nil, "setup_future_usage"=>nil, "shipping"=>nil, "source"=>nil, "statement_descriptor"=>nil, "statement_descriptor_suffix"=>nil, "status"=>"succeeded", "transfer_data"=>nil, "transfer_group"=>nil} + RESPONSE + end + + def successful_create_intent_response_with_google_pay_and_billing_address + <<-RESPONSE + {"id"=>"pi_3N0nKLAWOtgoysog3cRTGUqD", "object"=>"payment_intent", "amount"=>2000, "amount_capturable"=>0, "amount_details"=>{"tip"=>{}}, "amount_received"=>2000, "application"=>nil, "application_fee_amount"=>nil, "automatic_payment_methods"=>nil, "canceled_at"=>nil, "cancellation_reason"=>nil, "capture_method"=>"automatic", "charges"=>{"object"=>"list", "data"=>[{"id"=>"ch_3N0nKLAWOtgoysog3npJdWNI", "object"=>"charge", "amount"=>2000, "amount_captured"=>2000, "amount_refunded"=>0, "application"=>nil, "application_fee"=>nil, "application_fee_amount"=>nil, "balance_transaction"=>"txn_3N0nKLAWOtgoysog3ZAmtAMT", "billing_details"=>{"address"=>{"city"=>"Ottawa", "country"=>"CA", "line1"=>"456 My Street", "line2"=>"Apt 1", "postal_code"=>"K1C2N6", "state"=>"ON"}, "email"=>nil, "name"=>nil, "phone"=>nil}, "calculated_statement_descriptor"=>"SPREEDLY", "captured"=>true, "created"=>1682434726, "currency"=>"gbp", "customer"=>nil, "description"=>nil, "destination"=>nil, "dispute"=>nil, "disputed"=>false, "failure_balance_transaction"=>nil, "failure_code"=>nil, "failure_message"=>nil, "fraud_details"=>{}, "invoice"=>nil, "livemode"=>false, "metadata"=>{}, "on_behalf_of"=>nil, "order"=>nil, "outcome"=>{"network_status"=>"approved_by_network", "reason"=>nil, "risk_level"=>"normal", "risk_score"=>61, "seller_message"=>"Payment complete.", "type"=>"authorized"}, "paid"=>true, "payment_intent"=>"pi_3N0nKLAWOtgoysog3cRTGUqD", "payment_method"=>"pm_1N0nKLAWOtgoysoglKSvcZz9", "payment_method_details"=>{"card"=>{"brand"=>"visa", "checks"=>{"address_line1_check"=>"pass", "address_postal_code_check"=>"pass", "cvc_check"=>nil}, "country"=>"US", "ds_transaction_id"=>nil, "exp_month"=>9, "exp_year"=>2030, "fingerprint"=>"hfaVNMiXc0dYSiC5", "funding"=>"credit", "installments"=>nil, "last4"=>"0000", "mandate"=>nil, "moto"=>nil, "network"=>"visa", "network_token"=>{"used"=>false}, "network_transaction_id"=>"104102978678771", "three_d_secure"=>nil, "wallet"=>{"dynamic_last4"=>"4242", "google_pay"=>{}, "type"=>"google_pay"}}, "type"=>"card"}, "receipt_email"=>nil, "receipt_number"=>nil, "receipt_url"=>"https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xNjBEWDZBV090Z295c29nKKbVn6IGMgbEjx6eavI6LBZciyBuj3wwsvIi6Fdr1gNyM807fxUBTGDg2j_1c42EB8vLZl4KcSJA0otk", "refunded"=>false, "refunds"=>{"object"=>"list", "data"=>[], "has_more"=>false, "total_count"=>0, "url"=>"/v1/charges/ch_3N0nKLAWOtgoysog3npJdWNI/refunds"}, "review"=>nil, "shipping"=>nil, "source"=>nil, "source_transfer"=>nil, "statement_descriptor"=>nil, "statement_descriptor_suffix"=>nil, "status"=>"succeeded", "transfer_data"=>nil, "transfer_group"=>nil}], "has_more"=>false, "total_count"=>1, "url"=>"/v1/charges?payment_intent=pi_3N0nKLAWOtgoysog3cRTGUqD"}, "client_secret"=>"pi_3N0nKLAWOtgoysog3cRTGUqD_secret_L4UFErMf6H4itOcZrZRqTwsuA", "confirmation_method"=>"automatic", "created"=>1682434725, "currency"=>"gbp", "customer"=>nil, "description"=>nil, "invoice"=>nil, "last_payment_error"=>nil, "latest_charge"=>"ch_3N0nKLAWOtgoysog3npJdWNI", "level3"=>nil, "livemode"=>false, "metadata"=>{}, "next_action"=>nil, "on_behalf_of"=>nil, "payment_method"=>"pm_1N0nKLAWOtgoysoglKSvcZz9", "payment_method_options"=>{"card"=>{"installments"=>nil, "mandate_options"=>nil, "network"=>nil, "request_three_d_secure"=>"automatic"}}, "payment_method_types"=>["card"], "processing"=>nil, "receipt_email"=>nil, "review"=>nil, "setup_future_usage"=>nil, "shipping"=>nil, "source"=>nil, "statement_descriptor"=>nil, "statement_descriptor_suffix"=>nil, "status"=>"succeeded", "transfer_data"=>nil, "transfer_group"=>nil} + RESPONSE + end + def successful_capture_response <<-RESPONSE {"id":"pi_1F1xauAWOtgoysogIfHO8jGi","object":"payment_intent","amount":2020,"amount_capturable":0,"amount_received":2020,"application":null,"application_fee_amount":null,"canceled_at":null,"cancellation_reason":null,"capture_method":"manual","charges":{"object":"list","data":[{"id":"ch_1F1xavAWOtgoysogxrtSiCu4","object":"charge","amount":2020,"amount_refunded":0,"application":null,"application_fee":null,"application_fee_amount":null,"balance_transaction":"txn_1F1xawAWOtgoysog27xGBjM6","billing_details":{"address":{"city":null,"country":null,"line1":null,"line2":null,"postal_code":null,"state":null},"email":null,"name":null,"phone":null},"captured":true,"created":1564501833,"currency":"gbp","customer":"cus_7s22nNueP2Hjj6","description":null,"destination":null,"dispute":null,"failure_code":null,"failure_message":null,"fraud_details":{},"invoice":null,"livemode":false,"metadata":{},"on_behalf_of":null,"order":null,"outcome":{"network_status":"approved_by_network","reason":null,"risk_level":"normal","risk_score":58,"seller_message":"Payment complete.","type":"authorized"},"paid":true,"payment_intent":"pi_1F1xauAWOtgoysogIfHO8jGi","payment_method":"pm_1F1xauAWOtgoysog00COoKIU","payment_method_details":{"card":{"brand":"visa","checks":{"address_line1_check":null,"address_postal_code_check":null,"cvc_check":null},"country":"US","exp_month":7,"exp_year":2020,"fingerprint":"hfaVNMiXc0dYSiC5","funding":"credit","last4":"4242","three_d_secure":null,"wallet":null},"type":"card"},"receipt_email":null,"receipt_number":null,"receipt_url":"https://pay.stripe.com/receipts/acct_160DX6AWOtgoysog/ch_1F1xavAWOtgoysogxrtSiCu4/rcpt_FX1eGdFRi8ssOY8Fqk4X6nEjNeGV5PG","refunded":false,"refunds":{"object":"list","data":[],"has_more":false,"total_count":0,"url":"/v1/charges/ch_1F1xavAWOtgoysogxrtSiCu4/refunds"},"review":null,"shipping":null,"source":null,"source_transfer":null,"statement_descriptor":null,"status":"succeeded","transfer_data":null,"transfer_group":null}],"has_more":false,"total_count":1,"url":"/v1/charges?payment_intent=pi_1F1xauAWOtgoysogIfHO8jGi"},"client_secret":"pi_1F1xauAWOtgoysogIfHO8jGi_secret_ZrXvfydFv0BelaMQJgHxjts5b","confirmation_method":"manual","created":1564501832,"currency":"gbp","customer":"cus_7s22nNueP2Hjj6","description":null,"invoice":null,"last_payment_error":null,"livemode":false,"metadata":{},"next_action":null,"on_behalf_of":null,"payment_method":"pm_1F1xauAWOtgoysog00COoKIU","payment_method_options":{"card":{"request_three_d_secure":"automatic"}},"payment_method_types":["card"],"receipt_email":null,"review":null,"setup_future_usage":null,"shipping":null,"source":null,"statement_descriptor":null,"status":"succeeded","transfer_data":null,"transfer_group":null} diff --git a/test/unit/gateways/stripe_test.rb b/test/unit/gateways/stripe_test.rb index 04ec96df420..1ce0e52c96c 100644 --- a/test/unit/gateways/stripe_test.rb +++ b/test/unit/gateways/stripe_test.rb @@ -4,7 +4,7 @@ class StripeTest < Test::Unit::TestCase include CommStub def setup - @gateway = StripeGateway.new(login: 'login') + @gateway = StripeGateway.new(login: 'sk_test_login') @credit_card = credit_card() @threeds_card = credit_card('4000000000003063') @@ -15,6 +15,7 @@ def setup @options = { billing_address: address(), statement_address: statement_address(), + shipping_address: shipping_address(), description: 'Test Purchase' } @@ -644,6 +645,15 @@ def test_successful_void_with_reason assert_success response end + def test_successful_void_with_reverse_transfer + @gateway.expects(:ssl_request).with do |_, _, post, _| + post.include?('reverse_transfer=true') + end.returns(successful_purchase_response(true)) + + assert response = @gateway.void('ch_test_charge', { reverse_transfer: true }) + assert_success response + end + def test_successful_refund @gateway.expects(:ssl_request).returns(successful_partially_refunded_response) @@ -866,6 +876,13 @@ def test_invalid_raw_response assert_match(/^Invalid response received from the Stripe API/, response.message) end + def test_invalid_login_test_transaction + gateway = StripeGateway.new(login: 'sk_live_3422') + assert response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match 'Invalid API Key provided', response.message + end + def test_add_creditcard_with_credit_card post = {} @gateway.send(:add_creditcard, post, @credit_card, {}) @@ -949,10 +966,12 @@ def test_add_creditcard_with_emv_credit_card def test_add_creditcard_pads_eci_value post = {} - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: '111111111100cryptogram', verification_value: nil, - eci: '7') + eci: '7' + ) @gateway.send(:add_creditcard, post, credit_card, {}) @@ -1148,6 +1167,31 @@ def test_add_statement_address assert_equal @options[:statement_address][:city], post[:statement_address][:city] end + def test_add_shipping_address + post = {} + + @gateway.send(:add_shipping_address, post, @credit_card, @options) + + assert_equal @options[:shipping_address][:zip], post[:shipping][:address][:postal_code] + assert_equal @options[:shipping_address][:state], post[:shipping][:address][:state] + assert_equal @options[:shipping_address][:address1], post[:shipping][:address][:line1] + assert_equal @options[:shipping_address][:address2], post[:shipping][:address][:line2] + assert_equal @options[:shipping_address][:country], post[:shipping][:address][:country] + assert_equal @options[:shipping_address][:city], post[:shipping][:address][:city] + assert_equal @options[:shipping_address][:name], post[:shipping][:name] + assert_equal @options[:shipping_address][:phone_number], post[:shipping][:phone] + end + + def test_shipping_address_not_added_if_no_name_present + post = {} + + options = @options.dup + options[:shipping_address] = options[:shipping_address].except(:name) + @gateway.send(:add_shipping_address, post, @credit_card, options) + + assert_empty post + end + def test_add_statement_address_returns_nil_if_required_fields_missing post = {} %i[address1 city zip state].each do |required_key| @@ -1219,7 +1263,7 @@ def test_optional_idempotency_on_verify end def test_initialize_gateway_with_version - @gateway = StripeGateway.new(login: 'login', version: '2013-12-03') + @gateway = StripeGateway.new(login: 'sk_test_login', version: '2013-12-03') @gateway.expects(:ssl_request).once.with { |_method, _url, _post, headers| headers && headers['Stripe-Version'] == '2013-12-03' }.returns(successful_purchase_response) @@ -1398,10 +1442,12 @@ def test_successful_auth_with_network_tokenization_apple_pay true end.returns(successful_authorization_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: '111111111100cryptogram', verification_value: nil, - eci: '05') + eci: '05' + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_instance_of Response, response @@ -1418,11 +1464,13 @@ def test_successful_auth_with_network_tokenization_android_pay true end.returns(successful_authorization_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: '111111111100cryptogram', verification_value: nil, eci: '05', - source: :android_pay) + source: :android_pay + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_instance_of Response, response @@ -1439,10 +1487,12 @@ def test_successful_purchase_with_network_tokenization_apple_pay true end.returns(successful_authorization_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: '111111111100cryptogram', verification_value: nil, - eci: '05') + eci: '05' + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_instance_of Response, response @@ -1459,11 +1509,13 @@ def test_successful_purchase_with_network_tokenization_android_pay true end.returns(successful_authorization_response) - credit_card = network_tokenization_credit_card('4242424242424242', + credit_card = network_tokenization_credit_card( + '4242424242424242', payment_cryptogram: '111111111100cryptogram', verification_value: nil, eci: '05', - source: :android_pay) + source: :android_pay + ) assert response = @gateway.purchase(@amount, credit_card, @options) assert_instance_of Response, response diff --git a/test/unit/gateways/sum_up_test.rb b/test/unit/gateways/sum_up_test.rb new file mode 100644 index 00000000000..5fc1995f19c --- /dev/null +++ b/test/unit/gateways/sum_up_test.rb @@ -0,0 +1,494 @@ +require 'test_helper' + +class SumUpTest < Test::Unit::TestCase + def setup + @gateway = SumUpGateway.new( + access_token: 'sup_sk_ABC123', + pay_to_email: 'example@example.com' + ) + @credit_card = credit_card + @amount = 100 + + @options = { + payment_type: 'card', + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_purchase + @gateway.expects(:ssl_request).returns(successful_create_checkout_response) + @gateway.expects(:ssl_request).returns(successful_complete_checkout_response) + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_success response + + assert_equal 'PENDING', response.message + refute_empty response.params + assert response.test? + end + + def test_failed_purchase + @gateway.expects(:ssl_request).returns(failed_complete_checkout_array_response) + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + + assert_equal SumUpGateway::STANDARD_ERROR_CODE_MAPPING[:multiple_invalid_parameters], response.error_code + end + + def test_successful_void + @gateway.expects(:ssl_request).returns(successful_void_response) + response = @gateway.void('c0887be5-9fd2-4018-a531-e573e0298fdd') + assert_success response + assert_equal 'EXPIRED', response.message + end + + def test_failed_void + @gateway.expects(:ssl_request).returns(failed_void_response) + response = @gateway.void('c0887be5-9fd2-4018-a531-e573e0298fdd22') + assert_failure response + assert_equal 'Resource not found', response.message + assert_equal 'NOT_FOUND', response.error_code + end + + def test_failed_refund + @gateway.expects(:ssl_request).returns(failed_refund_response) + response = @gateway.refund(nil, 'c0887be5-9fd2-4018-a531-e573e0298fdd22') + assert_failure response + assert_equal 'The transaction is not refundable in its current state', response.message + assert_equal 'CONFLICT', response.error_code + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_success_from + response = @gateway.send(:parse, successful_complete_checkout_response) + success_from = @gateway.send(:success_from, response.symbolize_keys) + assert_equal true, success_from + end + + def test_message_from + response = @gateway.send(:parse, successful_complete_checkout_response) + message_from = @gateway.send(:message_from, response.symbolize_keys) + assert_equal 'PENDING', message_from + end + + def test_authorization_from + response = @gateway.send(:parse, successful_complete_checkout_response) + authorization_from = @gateway.send(:authorization_from, response.symbolize_keys) + assert_equal '8d8336a1-32e2-4f96-820a-5c9ee47e76fc', authorization_from + end + + def test_format_multiple_errors + responses = @gateway.send(:parse, failed_complete_checkout_array_response) + error_code = @gateway.send(:format_multiple_errors, responses) + assert_equal format_multiple_errors_response, error_code + end + + def test_error_code_from + response = @gateway.send(:parse, failed_complete_checkout_response) + error_code_from = @gateway.send(:error_code_from, response.symbolize_keys) + assert_equal 'CHECKOUT_SESSION_IS_EXPIRED', error_code_from + end + + private + + def pre_scrubbed + <<-PRE_SCRUBBED + opening connection to api.sumup.com:443... + opened + starting SSL for api.sumup.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- \"POST /v0.1/checkouts HTTP/1.1\\r\ + Content-Type: application/json\\r\ + Authorization: Bearer sup_sk_ABC123\\r\ + Connection: close\\r\ + Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\ + Accept: */*\\r\ + User-Agent: Ruby\\r\ + Host: api.sumup.com\\r\ + Content-Length: 422\\r\ + \\r\ + \" + <- \"{\\\"pay_to_email\\\":\\\"example@example.com\\\",\\\"redirect_url\\\":null,\\\"return_url\\\":null,\\\"checkout_reference\\\":\\\"14c812fc-4689-4b8a-a4d7-ed21bf3c39ff\\\",\\\"amount\\\":\\\"1.00\\\",\\\"currency\\\":\\\"USD\\\",\\\"description\\\":\\\"Store Purchase\\\",\\\"personal_details\\\":{\\\"address\\\":{\\\"city\\\":\\\"Ottawa\\\",\\\"state\\\":\\\"ON\\\",\\\"country\\\":\\\"CA\\\",\\\"line_1\\\":\\\"456 My Street\\\",\\\"postal_code\\\":\\\"K1C2N6\\\"},\\\"email\\\":null,\\\"first_name\\\":\\\"Longbob\\\",\\\"last_name\\\":\\\"Longsen\\\",\\\"tax_id\\\":null},\\\"customer_id\\\":null}\" + -> \"HTTP/1.1 201 Created\\r\ + \" + -> \"Date: Thu, 14 Sep 2023 05:15:41 GMT\\r\ + \" + -> \"Content-Type: application/json;charset=UTF-8\\r\ + \" + -> \"Content-Length: 360\\r\ + \" + -> \"Connection: close\\r\ + \" + -> \"x-powered-by: Express\\r\ + \" + -> \"access-control-allow-origin: *\\r\ + \" + -> \"x-fong-id: 723b20084f2c, 723b20084f2c, 723b20084f2c 5df223126f1c\\r\ + \" + -> \"cf-cache-status: DYNAMIC\\r\ + \" + -> \"vary: Accept-Encoding\\r\ + \" + -> \"apigw-requestid: LOyHiheuDoEEJSA=\\r\ + \" + -> \"set-cookie: __cf_bm=1unGPonmyW_H8VRqo.O6h20hrSJ_0GtU3VqD9i3uYkI-1694668540-0-AaYQ1MVLyKxcwSNy8oNS5t/uVdk5ZU6aFPI/yvVcohm0Fm+Kltk55ngpG/Bms3cvRtxVX9DidO4ziiP2IsQcM41uJZq6TrcgLUD7KbJfJwV8; path=/; expires=Thu, 14-Sep-23 05:45:40 GMT; domain=.sumup.com; HttpOnly; Secure; SameSite=None\\r\ + \" + -> \"x-op-gateway: true\\r\ + \" + -> \"Set-Cookie: __cf_bm=OYzsPf_HGhiUfF0EETH_zOM74zPZpYhmqI.FJxehmpY-1694668541-0-AWVAexX304k53VB3HIhdyg+uP4ElzrS23jwIAdPGccfN5DM/81TE0ioW7jb7kA3jCZDuGENGofaZz0pBwSr66lRiWu9fdAzdUIbwNDOBivWY; path=/; expires=Thu, 14-Sep-23 05:45:41 GMT; domain=.sumup.com; HttpOnly; Secure; SameSite=None\\r\ + \" + -> \"Server: cloudflare\\r\ + \" + -> \"CF-RAY: 80662747af463995-BOG\\r\ + \" + -> \"\\r\ + \" + reading 360 bytes... + -> \"{\\\"checkout_reference\\\":\\\"14c812fc-4689-4b8a-a4d7-ed21bf3c39ff\\\",\\\"amount\\\":1.0,\\\"currency\\\":\\\"USD\\\",\\\"pay_to_email\\\":\\\"example@example.com\\\",\\\"merchant_code\\\":\\\"MTVU2XGK\\\",\\\"description\\\":\\\"Store Purchase\\\",\\\"id\\\":\\\"70f71869-ed81-40b0-b2d8-c98f80f4c39d\\\",\\\"status\\\":\\\"PENDING\\\",\\\"date\\\":\\\"2023-09-14T05:15:40.000+00:00\\\",\\\"merchant_name\\\":\\\"Spreedly\\\",\\\"purpose\\\":\\\"CHECKOUT\\\",\\\"transactions\\\":[]}\" + read 360 bytes + Conn close + opening connection to api.sumup.com:443... + opened + starting SSL for api.sumup.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- \"PUT /v0.1/checkouts/70f71869-ed81-40b0-b2d8-c98f80f4c39d HTTP/1.1\\r\ + Content-Type: application/json\\r\ + Authorization: Bearer sup_sk_ABC123\\r\ + Connection: close\\r\ + Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\ + Accept: */*\\r\ + User-Agent: Ruby\\r\ + Host: api.sumup.com\\r\ + Content-Length: 136\\r\ + \\r\ + \" + <- \"{\\\"payment_type\\\":\\\"card\\\",\\\"card\\\":{\\\"name\\\":\\\"Longbob Longsen\\\",\\\"number\\\":\\\"4000100011112224\\\",\\\"expiry_month\\\":\\\"09\\\",\\\"expiry_year\\\":\\\"24\\\",\\\"cvv\\\":\\\"123\\\"}}\" + -> \"HTTP/1.1 200 OK\\r\ + \" + -> \"Date: Thu, 14 Sep 2023 05:15:41 GMT\\r\ + \" + -> \"Content-Type: application/json\\r\ + \" + -> \"Transfer-Encoding: chunked\\r\ + \" + -> \"Connection: close\\r\ + \" + -> \"x-powered-by: Express\\r\ + \" + -> \"access-control-allow-origin: *\\r\ + \" + -> \"x-fong-id: 8a116d29420e, 8a116d29420e, 8a116d29420e a534b6871710\\r\ + \" + -> \"cf-cache-status: DYNAMIC\\r\ + \" + -> \"vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers\\r\ + \" + -> \"apigw-requestid: LOyHoggJjoEEMxA=\\r\ + \" + -> \"set-cookie: __cf_bm=AoWMlPJNg1_THatbGnZchhj7K0QaqwlU0SqYrlDJ.78-1694668541-0-AdHrPpd/94p0oyLJWzsEUYatqVZMiJ0i1BJICEiprAo8AMDiya+V3OjljwbCpaNQNAPFVJpX1S4KxIFEUEeeNfAJv1HOjjaToNYhJuhLQ1NT; path=/; expires=Thu, 14-Sep-23 05:45:41 GMT; domain=.sumup.com; HttpOnly; Secure; SameSite=None\\r\ + \" + -> \"x-op-gateway: true\\r\ + \" + -> \"Set-Cookie: __cf_bm=UcJRX.Pe233lWIyCGlqNICBOhruxwESN41sDCDfzQBQ-1694668541-0-ASJ/Wl84HRovjKIq/p+Re8GrxkxHM1XvbDE/mXT/4r7PYA1cpTzG2uhp7WEkqVpEj7FCb2ahP5ExApEWWx0JDut8Uhx1SeQJHYFR/26E8BTv; path=/; expires=Thu, 14-Sep-23 05:45:41 GMT; domain=.sumup.com; HttpOnly; Secure; SameSite=None\\r\ + \" + -> \"Server: cloudflare\\r\ + \" + -> \"CF-RAY: 8066274e3a95399b-BOG\\r\ + \" + -> \"Content-Encoding: gzip\\r\ + \" + -> \"\\r\ + \" + -> \"1bc\\r\ + \" + reading 444 bytes... + -> \"\\x1F\\x8B\\b\\x00\\x00\\x00\\x00\\x00\\x00\\x03|\\x92[\\x8B\\xDB0\\x10\\x85\\xFFJ\\x99\\xD7ZA\\x92\\x15G\\xD6S!1\\xDB\\xB2\\xCD\\x85\\x8D]RJ1\\xB2$wMm\\xD9Hr\\xC1,\\xFB\\xDF\\x8B\\xF6R\\x1A\\xBA\\xF4\\xF50G\\xF3\\xCD9z\\x00uo\\xD4\\xCFq\\x0E\\xB53\\xADq\\xC6*\\x03\\x02\\bS\\x9C\\xD0V!\\x96\\xF1\\x1C\\xB1\\x86K$\\x99\\xDE \\xA3)i\\xDAT\\xA5y\\xDBB\\x02r\\x18g\\e@\\x90\\x15N@\\xCD.\\xDA\\x17\\x10P\\x9Dw\\x90\\xC0$\\x97:\\x8C\\xB5\\x19d\\xD7\\x83\\x80\\xCE\\x06\\xF3\\xC3\\xC9\\xD0\\x8D\\xD6\\x7F\\xF0\\x933F\\xF7\\xCBJ\\x8D\\x03$0\\x18\\xA7\\xEE\\xA5\\r\\xB5\\x1Au\\xDC\\xBF/\\xBFT\\xF4rs\\v\\th\\xE3\\x95\\xEB\\xA6h\\x03\\x01\\xE70:\\xF3\\xEE4\\xC7qo \\x81N\\x83\\x80\\rn7\\x84g92\\x9A\\x13\\xC4p\\x83QC5G*\\xE7-\\xC7-Si\\xAE!\\x01\\x1Fd\\x98=\\b8\\x15\\x87\\xDD\\xA7\\xC3M|]\\x86\\xB8\\x8Fb\\x9A\\\"\\x9C#\\xC2J\\xBC\\x16d-\\x18^a\\x8C\\xDFc,0\\xFE\\x9B\\xCF\\xCA!\\xCE\\x9F_\\xF0\\xE3\\x95\\xB3\\x9BF\\x1F\\xC5\\xED\\xC7b{{\\xACJH 8i\\xBDTO\\xB7\\x82\\xF8\\xF6\\xF0\\x8C\\x893\\xCD\\x15[S\\xD4\\xB2\\xD4 \\x96R\\x8E8\\xC7\" + -> \")\\xE2\\xBAU\\x9A\\xF0\\x94\\xD0&\\xBD6\\xBF\\xE6Q\\xEE(\\xADN\\x97\\xCF\\x97\\xF2\\xFFa]\\x15\\xF2K\\x86\\xFAU\\xC0Q\\b\\xDDt-\\xFCSY\\xE8\\x06\\xE3\\x83\\x1C\\xA673!+\\xC6\\xF3?\\x99\\xBC\\x91\\xE6$\\x97\\xC1\\xD8P\\x87e\\x8A`\\xC5\\xF6\\xB8\\x87\\x04\\x8C\\rn\\xA9\\x87g\\xD8mu.\\x8F\\xFB\\xE2\\xAE.\\x0E\\xE5\\xDD\\xD7X\\xA0\\xF5A\\xF6}\\xF4\\xF9Z\\xBD\\xE0'O\\xBF\\xC5Y\\xD9\\xD71\\xB95\\xC9\\xE8\\x06\\xA7,\\xA3\\x8F\\xDF\\x1F\\x7F\\x03\\x00\\x00\\xFF\\xFF\\x03\\x00\\xB5\\x12\\xCA\\x11\\xB3\\x02\\x00\\x00\" + read 444 bytes + reading 2 bytes... + -> \"\\r\ + \" + read 2 bytes + -> \"0\\r\ + \" + -> \"\\r\ + \" + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SCRUBBED + opening connection to api.sumup.com:443... + opened + starting SSL for api.sumup.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- \"POST /v0.1/checkouts HTTP/1.1\\r\ + Content-Type: application/json\\r\ + Authorization: Bearer [FILTERED]\\r\ + Connection: close\\r\ + Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\ + Accept: */*\\r\ + User-Agent: Ruby\\r\ + Host: api.sumup.com\\r\ + Content-Length: 422\\r\ + \\r\ + \" + <- \"{\\\"pay_to_email\\\":\\\"[FILTERED]\",\\\"redirect_url\\\":null,\\\"return_url\\\":null,\\\"checkout_reference\\\":\\\"14c812fc-4689-4b8a-a4d7-ed21bf3c39ff\\\",\\\"amount\\\":\\\"1.00\\\",\\\"currency\\\":\\\"USD\\\",\\\"description\\\":\\\"Store Purchase\\\",\\\"personal_details\\\":{\\\"address\\\":{\\\"city\\\":\\\"Ottawa\\\",\\\"state\\\":\\\"ON\\\",\\\"country\\\":\\\"CA\\\",\\\"line_1\\\":\\\"456 My Street\\\",\\\"postal_code\\\":\\\"K1C2N6\\\"},\\\"email\\\":null,\\\"first_name\\\":\\\"Longbob\\\",\\\"last_name\\\":\\\"Longsen\\\",\\\"tax_id\\\":null},\\\"customer_id\\\":null}\" + -> \"HTTP/1.1 201 Created\\r\ + \" + -> \"Date: Thu, 14 Sep 2023 05:15:41 GMT\\r\ + \" + -> \"Content-Type: application/json;charset=UTF-8\\r\ + \" + -> \"Content-Length: 360\\r\ + \" + -> \"Connection: close\\r\ + \" + -> \"x-powered-by: Express\\r\ + \" + -> \"access-control-allow-origin: *\\r\ + \" + -> \"x-fong-id: 723b20084f2c, 723b20084f2c, 723b20084f2c 5df223126f1c\\r\ + \" + -> \"cf-cache-status: DYNAMIC\\r\ + \" + -> \"vary: Accept-Encoding\\r\ + \" + -> \"apigw-requestid: LOyHiheuDoEEJSA=\\r\ + \" + -> \"set-cookie: __cf_bm=1unGPonmyW_H8VRqo.O6h20hrSJ_0GtU3VqD9i3uYkI-1694668540-0-AaYQ1MVLyKxcwSNy8oNS5t/uVdk5ZU6aFPI/yvVcohm0Fm+Kltk55ngpG/Bms3cvRtxVX9DidO4ziiP2IsQcM41uJZq6TrcgLUD7KbJfJwV8; path=/; expires=Thu, 14-Sep-23 05:45:40 GMT; domain=.sumup.com; HttpOnly; Secure; SameSite=None\\r\ + \" + -> \"x-op-gateway: true\\r\ + \" + -> \"Set-Cookie: __cf_bm=OYzsPf_HGhiUfF0EETH_zOM74zPZpYhmqI.FJxehmpY-1694668541-0-AWVAexX304k53VB3HIhdyg+uP4ElzrS23jwIAdPGccfN5DM/81TE0ioW7jb7kA3jCZDuGENGofaZz0pBwSr66lRiWu9fdAzdUIbwNDOBivWY; path=/; expires=Thu, 14-Sep-23 05:45:41 GMT; domain=.sumup.com; HttpOnly; Secure; SameSite=None\\r\ + \" + -> \"Server: cloudflare\\r\ + \" + -> \"CF-RAY: 80662747af463995-BOG\\r\ + \" + -> \"\\r\ + \" + reading 360 bytes... + -> \"{\\\"checkout_reference\\\":\\\"14c812fc-4689-4b8a-a4d7-ed21bf3c39ff\\\",\\\"amount\\\":1.0,\\\"currency\\\":\\\"USD\\\",\\\"pay_to_email\\\":\\\"[FILTERED]\",\\\"merchant_code\\\":\\\"MTVU2XGK\\\",\\\"description\\\":\\\"Store Purchase\\\",\\\"id\\\":\\\"70f71869-ed81-40b0-b2d8-c98f80f4c39d\\\",\\\"status\\\":\\\"PENDING\\\",\\\"date\\\":\\\"2023-09-14T05:15:40.000+00:00\\\",\\\"merchant_name\\\":\\\"Spreedly\\\",\\\"purpose\\\":\\\"CHECKOUT\\\",\\\"transactions\\\":[]}\" + read 360 bytes + Conn close + opening connection to api.sumup.com:443... + opened + starting SSL for api.sumup.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- \"PUT /v0.1/checkouts/70f71869-ed81-40b0-b2d8-c98f80f4c39d HTTP/1.1\\r\ + Content-Type: application/json\\r\ + Authorization: Bearer [FILTERED]\\r\ + Connection: close\\r\ + Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\ + Accept: */*\\r\ + User-Agent: Ruby\\r\ + Host: api.sumup.com\\r\ + Content-Length: 136\\r\ + \\r\ + \" + <- \"{\\\"payment_type\\\":\\\"card\\\",\\\"card\\\":{\\\"name\\\":\\\"Longbob Longsen\\\",\\\"number\\\":\\\"[FILTERED]\",\\\"expiry_month\\\":\\\"09\\\",\\\"expiry_year\\\":\\\"24\\\",\\\"cvv\\\":\\\"[FILTERED]\"}}\" + -> \"HTTP/1.1 200 OK\\r\ + \" + -> \"Date: Thu, 14 Sep 2023 05:15:41 GMT\\r\ + \" + -> \"Content-Type: application/json\\r\ + \" + -> \"Transfer-Encoding: chunked\\r\ + \" + -> \"Connection: close\\r\ + \" + -> \"x-powered-by: Express\\r\ + \" + -> \"access-control-allow-origin: *\\r\ + \" + -> \"x-fong-id: 8a116d29420e, 8a116d29420e, 8a116d29420e a534b6871710\\r\ + \" + -> \"cf-cache-status: DYNAMIC\\r\ + \" + -> \"vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers\\r\ + \" + -> \"apigw-requestid: LOyHoggJjoEEMxA=\\r\ + \" + -> \"set-cookie: __cf_bm=AoWMlPJNg1_THatbGnZchhj7K0QaqwlU0SqYrlDJ.78-1694668541-0-AdHrPpd/94p0oyLJWzsEUYatqVZMiJ0i1BJICEiprAo8AMDiya+V3OjljwbCpaNQNAPFVJpX1S4KxIFEUEeeNfAJv1HOjjaToNYhJuhLQ1NT; path=/; expires=Thu, 14-Sep-23 05:45:41 GMT; domain=.sumup.com; HttpOnly; Secure; SameSite=None\\r\ + \" + -> \"x-op-gateway: true\\r\ + \" + -> \"Set-Cookie: __cf_bm=UcJRX.Pe233lWIyCGlqNICBOhruxwESN41sDCDfzQBQ-1694668541-0-ASJ/Wl84HRovjKIq/p+Re8GrxkxHM1XvbDE/mXT/4r7PYA1cpTzG2uhp7WEkqVpEj7FCb2ahP5ExApEWWx0JDut8Uhx1SeQJHYFR/26E8BTv; path=/; expires=Thu, 14-Sep-23 05:45:41 GMT; domain=.sumup.com; HttpOnly; Secure; SameSite=None\\r\ + \" + -> \"Server: cloudflare\\r\ + \" + -> \"CF-RAY: 8066274e3a95399b-BOG\\r\ + \" + -> \"Content-Encoding: gzip\\r\ + \" + -> \"\\r\ + \" + -> \"1bc\\r\ + \" + reading 444 bytes... + -> \"\\x1F\\x8B\\b\\x00\\x00\\x00\\x00\\x00\\x00\\x03|\\x92[\\x8B\\xDB0\\x10\\x85\\xFFJ\\x99\\xD7ZA\\x92\\x15G\\xD6S!1\\xDB\\xB2\\xCD\\x85\\x8D]RJ1\\xB2$wMm\\xD9Hr\\xC1,\\xFB\\xDF\\x8B\\xF6R\\x1A\\xBA\\xF4\\xF50G\\xF3\\xCD9z\\x00uo\\xD4\\xCFq\\x0E\\xB53\\xADq\\xC6*\\x03\\x02\\bS\\x9C\\xD0V!\\x96\\xF1\\x1C\\xB1\\x86K$\\x99\\xDE \\xA3)i\\xDAT\\xA5y\\xDBB\\x02r\\x18g\\e@\\x90\\x15N@\\xCD.\\xDA\\x17\\x10P\\x9Dw\\x90\\xC0$\\x97:\\x8C\\xB5\\x19d\\xD7\\x83\\x80\\xCE\\x06\\xF3\\xC3\\xC9\\xD0\\x8D\\xD6\\x7F\\xF0\\x933F\\xF7\\xCBJ\\x8D\\x03$0\\x18\\xA7\\xEE\\xA5\\r\\xB5\\x1Au\\xDC\\xBF/\\xBFT\\xF4rs\\v\\th\\xE3\\x95\\xEB\\xA6h\\x03\\x01\\xE70:\\xF3\\xEE4\\xC7qo \\x81N\\x83\\x80\\rn7\\x84g92\\x9A\\x13\\xC4p\\x83QC5G*\\xE7-\\xC7-Si\\xAE!\\x01\\x1Fd\\x98=\\b8\\x15\\x87\\xDD\\xA7\\xC3M|]\\x86\\xB8\\x8Fb\\x9A\\\"\\x9C#\\xC2J\\xBC\\x16d-\\x18^a\\x8C\\xDFc,0\\xFE\\x9B\\xCF\\xCA!\\xCE\\x9F_\\xF0\\xE3\\x95\\xB3\\x9BF\\x1F\\xC5\\xED\\xC7b{{\\xACJH 8i\\xBDTO\\xB7\\x82\\xF8\\xF6\\xF0\\x8C\\x893\\xCD\\x15[S\\xD4\\xB2\\xD4 \\x96R\\x8E8\\xC7\" + -> \")\\xE2\\xBAU\\x9A\\xF0\\x94\\xD0&\\xBD6\\xBF\\xE6Q\\xEE(\\xADN\\x97\\xCF\\x97\\xF2\\xFFa]\\x15\\xF2K\\x86\\xFAU\\xC0Q\\b\\xDDt-\\xFCSY\\xE8\\x06\\xE3\\x83\\x1C\\xA673!+\\xC6\\xF3?\\x99\\xBC\\x91\\xE6$\\x97\\xC1\\xD8P\\x87e\\x8A`\\xC5\\xF6\\xB8\\x87\\x04\\x8C\\rn\\xA9\\x87g\\xD8mu.\\x8F\\xFB\\xE2\\xAE.\\x0E\\xE5\\xDD\\xD7X\\xA0\\xF5A\\xF6}\\xF4\\xF9Z\\xBD\\xE0'O\\xBF\\xC5Y\\xD9\\xD71\\xB95\\xC9\\xE8\\x06\\xA7,\\xA3\\x8F\\xDF\\x1F\\x7F\\x03\\x00\\x00\\xFF\\xFF\\x03\\x00\\xB5\\x12\\xCA\\x11\\xB3\\x02\\x00\\x00\" + read 444 bytes + reading 2 bytes... + -> \"\\r\ + \" + read 2 bytes + -> \"0\\r\ + \" + -> \"\\r\ + \" + Conn close + POST_SCRUBBED + end + + def successful_create_checkout_response + <<-RESPONSE + { + "checkout_reference": "e86ba553-b3d0-49f6-b4b5-18bd67502db2", + "amount": 1.0, + "currency": "USD", + "pay_to_email": "example@example.com", + "merchant_code": "ABC123", + "description": "Store Purchase", + "id": "8d8336a1-32e2-4f96-820a-5c9ee47e76fc", + "status": "PENDING", + "date": "2023-09-14T00:26:37.000+00:00", + "merchant_name": "Spreedly", + "purpose": "CHECKOUT", + "transactions": [] + } + RESPONSE + end + + def successful_complete_checkout_response + <<-RESPONSE + { + "checkout_reference": "e86ba553-b3d0-49f6-b4b5-18bd67502db2", + "amount": 1.0, + "currency": "USD", + "pay_to_email": "example@example.com", + "merchant_code": "ABC123", + "description": "Store Purchase", + "id": "8d8336a1-32e2-4f96-820a-5c9ee47e76fc", + "status": "PENDING", + "date": "2023-09-14T00: 26: 37.000+00: 00", + "merchant_name": "Spreedly", + "purpose": "CHECKOUT", + "transactions": [{ + "id": "1bce6072-1865-4a90-887f-cb7fda97b300", + "transaction_code": "TDMNUPS33H", + "merchant_code": "MTVU2XGK", + "amount": 1.0, + "vat_amount": 0.0, + "tip_amount": 0.0, + "currency": "USD", + "timestamp": "2023-09-14T00:26:38.420+00:00", + "status": "PENDING", + "payment_type": "ECOM", + "entry_mode": "CUSTOMER_ENTRY", + "installments_count": 1, + "internal_id": 5162527027 + }] + } + RESPONSE + end + + def failed_complete_checkout_response + <<-RESPONSE + { + "type": "https://developer.sumup.com/docs/problem/session-expired/", + "title": "Conflict", + "status": 409, + "detail": "The checkout session 79c866c2-0b2d-470d-925a-37ddc8855ec2 is expired", + "instance": "79a4ed94d177, 79a4ed94d177 c24ac3136c71", + "error_code": "CHECKOUT_SESSION_IS_EXPIRED", + "message": "Checkout is expired" + } + RESPONSE + end + + def failed_complete_checkout_array_response + <<-RESPONSE + [ + { + "message": "Validation error", + "param": "card", + "error_code": "The card is expired" + } + ] + RESPONSE + end + + def successful_void_response + <<-RESPONSE + { + "checkout_reference": "b5a47552-50e0-4c6e-af23-2495124b5091", + "id": "c0887be5-9fd2-4018-a531-e573e0298fdd", + "amount": 100.00, + "currency": "USD", + "pay_to_email": "integrations@spreedly.com", + "merchant_code": "MTVU2XGK", + "description": "Sample one-time payment", + "purpose": "CHECKOUT", + "status": "EXPIRED", + "date": "2023-09-14T16:32:39.200+00:00", + "valid_until": "2023-09-14T18:08:49.977+00:00", + "merchant_name": "Spreedly", + "transactions": [{ + "id": "fc805fc9-4864-4c6d-8e29-630c171fce54", + "transaction_code": "TDYEQ2RQ23", + "merchant_code": "MTVU2XGK", + "amount": 100.0, + "vat_amount": 0.0, + "tip_amount": 0.0, + "currency": "USD", + "timestamp": "2023-09-14T16:32:50.111+00:00", + "status": "CANCELLED", + "payment_type": "ECOM", + "entry_mode": "CUSTOMER_ENTRY", + "installments_count": 1, + "internal_id": 5165839144 + }] + } + RESPONSE + end + + def failed_void_response + <<-RESPONSE + { + "type": "https://developer.sumup.com/docs/problem/checkout-not-found/", + "title": "Not Found", + "status": 404, + "detail": "A checkout session with the id c0887be5-9fd2-4018-a531-e573e0298fdd22 does not exist", + "instance": "5e07254b2f25, 5e07254b2f25 a30463b627e3", + "error_code": "NOT_FOUND", + "message": "Resource not found" + } + RESPONSE + end + + def failed_refund_response + <<-RESPONSE + { + "message": "The transaction is not refundable in its current state", + "error_code": "CONFLICT" + } + RESPONSE + end + + def format_multiple_errors_response + { + error_code: 'MULTIPLE_INVALID_PARAMETERS', + message: 'Validation error', + errors: [{ error_code: 'The card is expired', param: 'card' }] + } + end +end diff --git a/test/unit/gateways/tns_test.rb b/test/unit/gateways/tns_test.rb index df59c9a62af..d7939f10630 100644 --- a/test/unit/gateways/tns_test.rb +++ b/test/unit/gateways/tns_test.rb @@ -157,7 +157,7 @@ def test_unsuccessful_verify assert_equal 'FAILURE - DECLINED', response.message end - def test_north_america_region_url + def test__url @gateway = TnsGateway.new( userid: 'userid', password: 'password', @@ -167,39 +167,7 @@ def test_north_america_region_url response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, @options) end.check_request do |_method, endpoint, _data, _headers| - assert_match(/secure.na.tnspayments.com/, endpoint) - end.respond_with(successful_capture_response) - - assert_success response - end - - def test_asia_pacific_region_url - @gateway = TnsGateway.new( - userid: 'userid', - password: 'password', - region: 'asia_pacific' - ) - - response = stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |_method, endpoint, _data, _headers| - assert_match(/secure.ap.tnspayments.com/, endpoint) - end.respond_with(successful_capture_response) - - assert_success response - end - - def test_europe_region_url - @gateway = TnsGateway.new( - userid: 'userid', - password: 'password', - region: 'europe' - ) - - response = stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |_method, endpoint, _data, _headers| - assert_match(/secure.eu.tnspayments.com/, endpoint) + assert_match(/secure.uat.tnspayments.com/, endpoint) end.respond_with(successful_capture_response) assert_success response diff --git a/test/unit/gateways/trans_first_transaction_express_test.rb b/test/unit/gateways/trans_first_transaction_express_test.rb index 61a0127d72e..38521f93391 100644 --- a/test/unit/gateways/trans_first_transaction_express_test.rb +++ b/test/unit/gateways/trans_first_transaction_express_test.rb @@ -257,6 +257,15 @@ def test_empty_response_fails assert_equal nil, response.message end + def test_transaction_code_xml_tag_added_with_correct_prefix + stub_comms do + @gateway.purchase(@amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + doc = Nokogiri::XML(data) + assert_not_empty doc.xpath('//v1:tranCode', 'v1' => 'http://postilion/realtime/merchantframework/xsd/v1/') + end.respond_with(successful_purchase_response) + end + def test_transcript_scrubbing assert_equal scrubbed_transcript, @gateway.scrub(transcript) end diff --git a/test/unit/gateways/trust_commerce_test.rb b/test/unit/gateways/trust_commerce_test.rb index fd5a6b33538..9d1782e4e51 100644 --- a/test/unit/gateways/trust_commerce_test.rb +++ b/test/unit/gateways/trust_commerce_test.rb @@ -167,6 +167,22 @@ def test_transcript_scrubbing_echeck assert_equal scrubbed_echeck_transcript, @gateway.scrub(echeck_transcript) end + def test_successful_verify + stub_comms do + @gateway.verify(@credit_card) + end.check_request do |_endpoint, data, _headers| + assert_match(%r{action=verify}, data) + end.respond_with(successful_verify_response) + end + + def test_unsuccessful_verify + bad_credit_card = credit_card('42909090990') + @gateway.expects(:ssl_post).returns(unsuccessful_verify_response) + assert response = @gateway.verify(bad_credit_card) + assert_instance_of Response, response + assert_failure response + end + private def successful_authorize_response @@ -235,6 +251,23 @@ def successful_unstore_response RESPONSE end + def successful_verify_response + <<~RESPONSE + transid=039-0170402443 + status=approved + avs=0 + cvv=M + RESPONSE + end + + def unsuccessful_verify_response + <<~RESPONSE + offenders=cc + error=badformat + status=baddata + RESPONSE + end + def transcript <<~TRANSCRIPT action=sale&demo=y&password=password&custid=TestMerchant&shipto_zip=90001&shipto_state=CA&shipto_city=Somewhere&shipto_address1=123+Test+St.&avs=n&zip=90001&state=CA&city=Somewhere&address1=123+Test+St.&cvv=1234&exp=0916&cc=4111111111111111&name=Longbob+Longsen&media=cc&ip=10.10.10.10&email=cody%40example.com&ticket=%231000.1&amount=100 diff --git a/test/unit/gateways/vanco_test.rb b/test/unit/gateways/vanco_test.rb index fed06cb0bd8..7fe1eafa753 100644 --- a/test/unit/gateways/vanco_test.rb +++ b/test/unit/gateways/vanco_test.rb @@ -3,6 +3,8 @@ class VancoTest < Test::Unit::TestCase include CommStub + SECONDS_PER_DAY = 3600 * 24 + def setup @gateway = VancoGateway.new(user_id: 'login', password: 'password', client_id: 'client_id') @credit_card = credit_card @@ -47,6 +49,54 @@ def test_successful_purchase_with_ip_address assert_success response end + def test_successful_purchase_with_existing_session_id + response = stub_comms do + previous_login_response = @gateway.send(:login) + @gateway.purchase( + @amount, + @credit_card, + @options.merge( + session_id: + { + id: '5d4177ec3c356ec5f7abe0e17e814250', + created_at: Time.parse(previous_login_response.params['auth_requesttime']) + } + ) + ) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(5d4177ec3c356ec5f7abe0e17e814250<\/SessionID>), data) if data =~ /EFTAddCompleteTransaction/ + end.respond_with(login_request_response, successful_purchase_with_existing_session_id_response) + + assert_success response + assert_equal '14949117|15756594|16136938', response.authorization + assert_equal 'Success', response.message + assert response.test? + end + + def test_successful_purchase_with_expired_session_id + two_days_from_now = Time.now - (2 * SECONDS_PER_DAY) + response = stub_comms do + @gateway.purchase( + @amount, + @credit_card, + @options.merge( + session_id: + { + id: 'f34177ec3c356ec5f7abe0e17e814250', + created_at: two_days_from_now + } + ) + ) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(5d8b104c9d8265db46bdf35ae9685472f4789dc8<\/SessionID>), data) if data =~ /EFTAddCompleteTransaction/ + end.respond_with(successful_login_response, successful_purchase_response) + + assert_success response + assert_equal '14949117|15756594|16136938', response.authorization + assert_equal 'Success', response.message + assert response.test? + end + def test_failed_purchase response = stub_comms do @gateway.purchase(@amount, @credit_card, @options) @@ -139,6 +189,12 @@ def successful_purchase_response ) end + def successful_purchase_with_existing_session_id_response + %( + ad4cbab9740e909423a02e622689d62015-05-01 16:08:07 -0400EFTAddCompleteTransaction5d4177ec3c356ec5f7abe0e17e81425022015-05-011494911715756594161369383.20 + ) + end + def successful_purchase_with_fund_id_response %( 8cf42301416298c9d6d71a39b27a0d2015-05-05 15:45:57 -0400EFTAddCompleteTransactionb2ec96e366f38a5c1ecd3f5343475526beaba4f922015-05-051494911715756594161373313.20\n\n @@ -180,4 +236,10 @@ def failed_refund_response dc9a5e2b620eee5d248e1b33cc1f332015-05-01 16:19:33 -0400EFTAddCredit67a731057f821413155033bc23551aef3ba0b2042575Amount Cannot Be Greater Than $100.05 ) end + + def login_request_response + %( + 38de95050240d49f8897578825c717 #{Time.now} Login 2 9edc7951048106b821f5e304e9ccdcc0700f533a + ) + end end diff --git a/test/unit/gateways/visanet_peru_test.rb b/test/unit/gateways/visanet_peru_test.rb index 81186a72ad9..896f2bbb5a9 100644 --- a/test/unit/gateways/visanet_peru_test.rb +++ b/test/unit/gateways/visanet_peru_test.rb @@ -1,4 +1,5 @@ require 'test_helper' +require 'timecop' class VisanetPeruTest < Test::Unit::TestCase include CommStub @@ -40,26 +41,38 @@ def test_failed_purchase end def test_nonconsecutive_purchase_numbers - pn1, pn2 = nil - - response1 = stub_comms(@gateway, :ssl_request) do - @gateway.authorize(@amount, @credit_card, @options) - end.check_request do |_method, _endpoint, data, _headers| - pn1 = JSON.parse(data)['purchaseNumber'] - end.respond_with(successful_authorize_response) - - # unit test is unrealistically speedy relative to real-world performance - sleep 0.1 - - response2 = stub_comms(@gateway, :ssl_request) do - @gateway.authorize(@amount, @credit_card, @options) - end.check_request do |_method, _endpoint, data, _headers| - pn2 = JSON.parse(data)['purchaseNumber'] - end.respond_with(successful_authorize_response) - - assert_success response1 - assert_success response2 - assert_not_equal(pn1, pn2) + purchase_times = [] + + Timecop.freeze do + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + purchase_times << JSON.parse(data)['purchaseNumber'].to_i + end.respond_with(successful_authorize_response) + + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + purchase_times << JSON.parse(data)['purchaseNumber'].to_i + end.respond_with(successful_authorize_response) + + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + purchase_times << JSON.parse(data)['purchaseNumber'].to_i + end.respond_with(successful_authorize_response) + + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + purchase_times << JSON.parse(data)['purchaseNumber'].to_i + end.respond_with(successful_authorize_response) + end + + purchase_times.each do |t| + assert_equal(t.to_s.length, 12) + end + assert_equal(purchase_times.uniq.size, purchase_times.size) end def test_successful_authorize diff --git a/test/unit/gateways/vpos_test.rb b/test/unit/gateways/vpos_test.rb index d321efed107..ebbe5bf96ad 100644 --- a/test/unit/gateways/vpos_test.rb +++ b/test/unit/gateways/vpos_test.rb @@ -12,7 +12,7 @@ def one_time_public_key class VposTest < Test::Unit::TestCase def setup - @gateway = VposGateway.new(public_key: 'some_key', private_key: 'some_other_key') + @gateway = VposGateway.new(public_key: 'some_key', private_key: 'some_other_key', encryption_key: OpenSSL::PKey::RSA.new(512)) @credit_card = credit_card @amount = 10000 diff --git a/test/unit/gateways/webpay_test.rb b/test/unit/gateways/webpay_test.rb index 474479742f1..66176804c87 100644 --- a/test/unit/gateways/webpay_test.rb +++ b/test/unit/gateways/webpay_test.rb @@ -4,7 +4,7 @@ class WebpayTest < Test::Unit::TestCase include CommStub def setup - @gateway = WebpayGateway.new(login: 'login') + @gateway = WebpayGateway.new(login: 'sk_test_login') @credit_card = credit_card() @amount = 40000 diff --git a/test/unit/gateways/wompi_test.rb b/test/unit/gateways/wompi_test.rb index d5c6682d489..81a7d4683d5 100644 --- a/test/unit/gateways/wompi_test.rb +++ b/test/unit/gateways/wompi_test.rb @@ -8,7 +8,7 @@ def setup @prod_gateway = WompiGateway.new(prod_public_key: 'pub_prod_1234', prod_private_key: 'priv_prod_5678') @ambidextrous_gateway = WompiGateway.new(prod_public_key: 'pub_prod_1234', prod_private_key: 'priv_prod_5678', test_public_key: 'pub_test_1234', test_private_key: 'priv_test_5678') @credit_card = credit_card - @amount = 100 + @amount = 150000 @options = { order_id: '1', @@ -82,29 +82,58 @@ def test_failed_capture assert_equal 'La transacción fue rechazada (Sandbox)', response.message end - def test_successful_refund - @gateway.expects(:ssl_post).returns(successful_refund_response) + # def test_successful_refund + # @gateway.expects(:ssl_post).returns(successful_refund_response) - response = @gateway.refund(@amount, @credit_card, @options) + # response = @gateway.refund(@amount, @credit_card, @options) + # assert_success response + + # assert_equal '113879-1635301011-28454', response.authorization + # assert response.test? + # end + + # def test_failed_refund + # @gateway.expects(:ssl_post).returns(failed_refund_response) + + # response = @gateway.refund(@amount, @credit_card, @options) + # assert_failure response + # message = JSON.parse(response.message) + # assert_equal 'transaction_id Debe ser completado', message['transaction_id'].first + # end + + def test_successful_refund_to_void + response = stub_comms(@gateway) do + @gateway.refund(@amount, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + assert_match 'void_sync', endpoint + assert_match @amount.to_s, data + end.respond_with(successful_void_response) assert_success response - assert_equal '113879-1635301011-28454', response.authorization + assert_equal '113879-1635301067-17128', response.authorization assert response.test? end - def test_failed_refund - @gateway.expects(:ssl_post).returns(failed_refund_response) + def test_successful_partial_refund_to_void + response = stub_comms(@gateway) do + @gateway.refund(@amount - 50000, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + assert_match 'void_sync', endpoint + assert_match (@amount - 50000).to_s, data + end.respond_with(successful_void_response) + assert_success response - response = @gateway.refund(@amount, @credit_card, @options) - assert_failure response - message = JSON.parse(response.message) - assert_equal 'transaction_id Debe ser completado', message['transaction_id'].first + assert_equal '113879-1635301067-17128', response.authorization + assert response.test? end def test_successful_void - @gateway.expects(:ssl_post).returns(successful_void_response) - - response = @gateway.void(@amount, @options) + response = stub_comms(@gateway) do + @gateway.void(@amount, @options) + end.check_request do |endpoint, data, _headers| + assert_match 'void_sync', endpoint + assert_match '{}', data + end.respond_with(successful_void_response) assert_success response assert_equal '113879-1635301067-17128', response.authorization diff --git a/test/unit/gateways/worldpay_test.rb b/test/unit/gateways/worldpay_test.rb index 6b1c76fb08e..a256785be9d 100644 --- a/test/unit/gateways/worldpay_test.rb +++ b/test/unit/gateways/worldpay_test.rb @@ -12,27 +12,35 @@ def setup @amount = 100 @credit_card = credit_card('4242424242424242') @token = '|99411111780163871111|shopper|59424549c291397379f30c5c082dbed8' - @elo_credit_card = credit_card('4514 1600 0000 0008', + @elo_credit_card = credit_card( + '4514 1600 0000 0008', month: 10, year: 2020, first_name: 'John', last_name: 'Smith', verification_value: '737', - brand: 'elo') - @nt_credit_card = network_tokenization_credit_card('4895370015293175', + brand: 'elo' + ) + @nt_credit_card = network_tokenization_credit_card( + '4895370015293175', brand: 'visa', eci: 5, source: :network_token, - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') - @nt_credit_card_without_eci = network_tokenization_credit_card('4895370015293175', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @nt_credit_card_without_eci = network_tokenization_credit_card( + '4895370015293175', source: :network_token, - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') - @credit_card_with_two_digits_year = credit_card('4514 1600 0000 0008', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @credit_card_with_two_digits_year = credit_card( + '4514 1600 0000 0008', month: 10, year: 22, first_name: 'John', last_name: 'Smith', - verification_value: '737') + verification_value: '737' + ) @sodexo_voucher = credit_card('6060704495764400', brand: 'sodexo') @options = { order_id: 1 } @store_options = { @@ -47,21 +55,144 @@ def setup } } - @apple_play_network_token = network_tokenization_credit_card('4895370015293175', + @apple_play_network_token = network_tokenization_credit_card( + '4895370015293175', month: 10, year: 24, first_name: 'John', last_name: 'Smith', verification_value: '737', - source: :apple_pay) + source: :apple_pay + ) - @google_pay_network_token = network_tokenization_credit_card('4444333322221111', + @google_pay_network_token = network_tokenization_credit_card( + '4444333322221111', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', month: '01', year: Time.new.year + 2, source: :google_pay, transaction_id: '123456789', - eci: '05') + eci: '05' + ) + + @level_two_data = { + level_2_data: { + invoice_reference_number: 'INV12233565', + customer_reference: 'CUST00000101', + card_acceptor_tax_id: 'VAT1999292', + sales_tax: { + amount: '20', + exponent: '2', + currency: 'USD' + }, + ship_from_postal_code: '43245', + destination_postal_code: '54545', + destination_country_code: 'CO', + order_date: { + day_of_month: Date.today.day, + month: Date.today.month, + year: Date.today.year + }, + tax_exempt: 'false' + } + } + + @level_three_data = { + level_3_data: { + customer_reference: 'CUST00000102', + card_acceptor_tax_id: 'VAT1999285', + sales_tax: { + amount: '20', + exponent: '2', + currency: 'USD' + }, + discount_amount: { + amount: '1', + exponent: '2', + currency: 'USD' + }, + shipping_amount: { + amount: '50', + exponent: '2', + currency: 'USD' + }, + duty_amount: { + amount: '20', + exponent: '2', + currency: 'USD' + }, + item: { + description: 'Laptop 14', + product_code: 'LP00125', + commodity_code: 'COM00125', + quantity: '2', + unit_cost: { + amount: '1500', + exponent: '2', + currency: 'USD' + }, + unit_of_measure: 'each', + item_total: { + amount: '3000', + exponent: '2', + currency: 'USD' + }, + item_total_with_tax: { + amount: '3500', + exponent: '2', + currency: 'USD' + }, + item_discount_amount: { + amount: '200', + exponent: '2', + currency: 'USD' + }, + tax_amount: { + amount: '500', + exponent: '2', + currency: 'USD' + } + } + } + } + end + + def test_payment_type_for_network_card + payment = @gateway.send(:payment_details, @nt_credit_card)[:payment_type] + assert_equal payment, :network_token + end + + def test_payment_type_returns_network_token_if_the_payment_method_responds_to_source_payment_cryptogram_and_eci + payment_method = mock + payment_method.stubs(source: nil, payment_cryptogram: nil, eci: nil) + result = @gateway.send(:payment_details, payment_method) + assert_equal({ payment_type: :network_token }, result) + end + + def test_payment_type_returns_credit_if_the_payment_method_does_not_responds_to_source + payment_method = mock + payment_method.stubs(payment_cryptogram: nil, eci: nil) + result = @gateway.send(:payment_details, payment_method) + assert_equal({ payment_type: :credit }, result) + end + + def test_payment_type_returns_credit_if_the_payment_method_does_not_responds_to_payment_cryptogram + payment_method = mock + payment_method.stubs(source: nil, eci: nil) + result = @gateway.send(:payment_details, payment_method) + assert_equal({ payment_type: :credit }, result) + end + + def test_payment_type_returns_credit_if_the_payment_method_does_not_responds_to_eci + payment_method = mock + payment_method.stubs(source: nil, payment_cryptogram: nil) + result = @gateway.send(:payment_details, payment_method) + assert_equal({ payment_type: :credit }, result) + end + + def test_payment_type_for_credit_card + payment = @gateway.send(:payment_details, @credit_card)[:payment_type] + assert_equal payment, :credit end def test_successful_authorize @@ -69,6 +200,20 @@ def test_successful_authorize @gateway.authorize(@amount, @credit_card, @options) end.check_request do |_endpoint, data, _headers| assert_match(/4242424242424242/, data) + assert_match(/cardHolderName/, data) + end.respond_with(successful_authorize_response) + assert_success response + assert_equal 'R50704213207145707', response.authorization + end + + def test_successful_authorize_without_name + credit_card = credit_card('4242424242424242', first_name: nil, last_name: nil) + response = stub_comms do + @gateway.authorize(@amount, credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/4242424242424242/, data) + assert_no_match(/cardHolderName/, data) + assert_match(/CARD-SSL/, data) end.respond_with(successful_authorize_response) assert_success response assert_equal 'R50704213207145707', response.authorization @@ -213,6 +358,41 @@ def test_successful_purchase assert_success response end + def test_transaction_with_level_two_data + options = @options.merge(@level_two_data) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(INV12233565), data + assert_match %r(CUST00000101), data + assert_match %r(VAT1999292), data + assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(43245), data + assert_match %r(54545), data + assert_match %r(CO), data + assert_match %r(false), data + assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(), data.gsub(/\s+/, '') + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_transaction_with_level_three_data + options = @options.merge(@level_three_data) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(CUST00000102), data + assert_match %r(VAT1999285), data + assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(Laptop14LP00125COM001252), data.gsub(/\s+/, '') + end.respond_with(successful_authorize_response) + assert_success response + end + def test_successful_purchase_with_sub_merchant_data options = @options.merge(@sub_merchant_options) response = stub_comms do @@ -302,6 +482,26 @@ def test_purchase_does_not_run_inquiry assert_equal(%w(authorize capture), response.responses.collect { |e| e.params['action'] }) end + def test_failed_purchase_with_issuer_response_code + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(failed_purchase_response_with_issuer_response_code) + + assert_failure response + assert_equal('51', response.params['issuer_response_code']) + assert_equal('Insufficient funds/over credit limit', response.params['issuer_response_description']) + end + + def test_failed_purchase_without_active_merchant_generated_response_message + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(failed_purchase_response_without_useful_error_from_gateway) + + assert_failure response + assert_equal('61', response.params['issuer_response_code']) + assert_equal('Exceeds withdrawal amount limit', response.message) + end + def test_successful_void response = stub_comms do @gateway.void(@options[:order_id], @options) @@ -476,9 +676,7 @@ def test_capture_time end.check_request do |_endpoint, data, _headers| if /capture/.match?(data) t = Time.now - assert_tag_with_attributes 'date', - { 'dayOfMonth' => t.day.to_s, 'month' => t.month.to_s, 'year' => t.year.to_s }, - data + assert_tag_with_attributes 'date', { 'dayOfMonth' => t.day.to_s, 'month' => t.month.to_s, 'year' => t.year.to_s }, data end end.respond_with(successful_inquiry_response, successful_capture_response) end @@ -487,9 +685,7 @@ def test_amount_handling stub_comms do @gateway.authorize(100, @credit_card, @options) end.check_request do |_endpoint, data, _headers| - assert_tag_with_attributes 'amount', - { 'value' => '100', 'exponent' => '2', 'currencyCode' => 'GBP' }, - data + assert_tag_with_attributes 'amount', { 'value' => '100', 'exponent' => '2', 'currencyCode' => 'GBP' }, data end.respond_with(successful_authorize_response) end @@ -497,17 +693,13 @@ def test_currency_exponent_handling stub_comms do @gateway.authorize(10000, @credit_card, @options.merge(currency: :JPY)) end.check_request do |_endpoint, data, _headers| - assert_tag_with_attributes 'amount', - { 'value' => '100', 'exponent' => '0', 'currencyCode' => 'JPY' }, - data + assert_tag_with_attributes 'amount', { 'value' => '100', 'exponent' => '0', 'currencyCode' => 'JPY' }, data end.respond_with(successful_authorize_response) stub_comms do @gateway.authorize(10000, @credit_card, @options.merge(currency: :OMR)) end.check_request do |_endpoint, data, _headers| - assert_tag_with_attributes 'amount', - { 'value' => '10000', 'exponent' => '3', 'currencyCode' => 'OMR' }, - data + assert_tag_with_attributes 'amount', { 'value' => '10000', 'exponent' => '3', 'currencyCode' => 'OMR' }, data end.respond_with(successful_authorize_response) end @@ -986,7 +1178,7 @@ def test_successful_store assert_match %r(4242424242424242), data assert_no_match %r(), data assert_no_match %r(), data - assert_no_match %r(), data + assert_no_match %r(), data end.respond_with(successful_store_response) assert_success response @@ -1238,6 +1430,26 @@ def test_order_id_crop_and_clean assert_success response end + def test_successful_inquire_with_order_id + response = stub_comms do + @gateway.inquire(nil, { order_id: @options[:order_id].to_s }) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('orderInquiry', { 'orderCode' => @options[:order_id].to_s }, data) + end.respond_with(successful_authorize_response) + assert_success response + assert_equal 'R50704213207145707', response.authorization + end + + def test_successful_inquire_with_authorization + response = stub_comms do + @gateway.inquire(@options[:order_id].to_s, {}) + end.check_request do |_endpoint, data, _headers| + assert_tag_with_attributes('orderInquiry', { 'orderCode' => @options[:order_id].to_s }, data) + end.respond_with(successful_authorize_response) + assert_success response + assert_equal 'R50704213207145707', response.authorization + end + private def assert_date_element(expected_date_hash, date_element) @@ -1432,6 +1644,37 @@ def failed_authorize_response_2 RESPONSE end + def failed_purchase_response_with_issuer_response_code + <<~RESPONSE + + + + + + + VISA-SSL + + REFUSED + + + + + + + + + + US + TEST BANK + + + + + + RESPONSE + end + def successful_capture_response <<~RESPONSE @@ -1666,6 +1909,35 @@ def failed_void_inquiry_response RESPONSE end + def failed_purchase_response_without_useful_error_from_gateway + <<~RESPONSE + + + + + + ECMC_DEBIT-SSL + + REFUSED + + + + + + + + + Snuffy Smith + US + PRETEND BANK + + + + + + RESPONSE + end + def successful_refund_inquiry_response(last_event = 'CAPTURED') <<~RESPONSE @@ -1868,7 +2140,7 @@ def sample_authorization_request Products Products Products - + 4242424242424242 @@ -1888,7 +2160,7 @@ def sample_authorization_request (555)555-5555 - + @@ -1911,7 +2183,7 @@ def transcript Purchase - + 4111111111111111 @@ -1927,7 +2199,7 @@ def transcript US - + wow@example.com @@ -1946,7 +2218,7 @@ def scrubbed_transcript Purchase - + [FILTERED] @@ -1962,7 +2234,7 @@ def scrubbed_transcript US - + wow@example.com diff --git a/test/unit/gateways/xpay_test.rb b/test/unit/gateways/xpay_test.rb new file mode 100644 index 00000000000..318e88d3de7 --- /dev/null +++ b/test/unit/gateways/xpay_test.rb @@ -0,0 +1,57 @@ +require 'test_helper' + +class XpayTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = XpayGateway.new( + api_key: 'some api key' + ) + @credit_card = credit_card + @amount = 100 + @base_url = @gateway.test_url + @options = {} + end + + def test_supported_countries + assert_equal %w(AT BE CY EE FI FR DE GR IE IT LV LT LU MT PT SK SI ES BG HR DK NO PL RO RO SE CH HU), XpayGateway.supported_countries + end + + def test_supported_cardtypes + assert_equal %i[visa master maestro american_express jcb], @gateway.supported_cardtypes + end + + def test_build_request_url_for_purchase + action = :purchase + assert_equal @gateway.send(:build_request_url, action), "#{@base_url}orders/2steps/payment" + end + + def test_build_request_url_with_id_param + action = :refund + id = 123 + assert_equal @gateway.send(:build_request_url, action, id), "#{@base_url}operations/{123}/refunds" + end + + def test_invalid_instance + assert_raise ArgumentError do + XpayGateway.new() + end + end + + def test_check_request_headers + stub_comms do + @gateway.send(:commit, 'purchase', {}, {}) + end.check_request(skip_response: true) do |_endpoint, _data, headers| + assert_equal headers['Content-Type'], 'application/json' + assert_equal headers['X-Api-Key'], 'some api key' + end + end + + def test_check_authorize_endpoint + stub_comms do + @gateway.send(:authorize, @amount, @credit_card, @options) + end.check_request(skip_response: true) do |endpoint, _data, _headers| + assert_match(/orders\/2steps\/init/, endpoint) + end + end +end diff --git a/test/unit/multi_response_test.rb b/test/unit/multi_response_test.rb index 2304bbb9a34..ed7c1935fd2 100644 --- a/test/unit/multi_response_test.rb +++ b/test/unit/multi_response_test.rb @@ -172,4 +172,94 @@ def test_handles_ignores_optional_request_result assert m.success? end + + def test_handles_responses_with_only_one_with_avs_and_cvv_result + r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'Y'), cvv_result: 'M' }) + r2 = Response.new(true, '2', {}) + m = MultiResponse.run do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => 'Y', 'message' => 'Street address and 5-digit postal code match.', 'street_match' => 'Y', 'postal_match' => 'Y' } + assert_equal m.cvv_result, { 'code' => 'M', 'message' => 'CVV matches' } + end + + def test_handles_responses_using_last_response_cvv_and_avs_result + r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'Y'), cvv_result: 'M' }) + r2 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'B'), cvv_result: 'N' }) + m = MultiResponse.run do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => 'B', 'message' => 'Street address matches, but postal code not verified.', 'street_match' => 'Y', 'postal_match' => nil } + assert_equal m.cvv_result, { 'code' => 'N', 'message' => 'CVV does not match' } + end + + def test_handles_responses_using_first_response_cvv_and_avs_result + r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'Y'), cvv_result: 'M' }) + r2 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'B'), cvv_result: 'N' }) + m = MultiResponse.run(:use_first_response) do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => 'Y', 'message' => 'Street address and 5-digit postal code match.', 'street_match' => 'Y', 'postal_match' => 'Y' } + assert_equal m.cvv_result, { 'code' => 'M', 'message' => 'CVV matches' } + end + + def test_handles_responses_using_first_response_cvv_that_no_has_cvv_and_avs_result + r1 = Response.new(true, '1', {}) + r2 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'B'), cvv_result: 'N' }) + m = MultiResponse.run(:use_first_response) do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => nil, 'message' => nil, 'street_match' => nil, 'postal_match' => nil } + assert_equal m.cvv_result, { 'code' => nil, 'message' => nil } + end + + def test_handles_response_with_avs_and_without_cvv_result + r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'X'), cvv_result: CVVResult.new(nil) }) + r2 = Response.new(true, '2', {}) + m = MultiResponse.run do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => 'X', 'message' => 'Street address and 9-digit postal code match.', 'street_match' => 'Y', 'postal_match' => 'Y' } + assert_equal m.cvv_result, { 'code' => nil, 'message' => nil } + end + + def test_handles_response_avs_and_cvv_result_with_wrong_values_avs_and_cvv_code + r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: '1234567'), cvv_result: CVVResult.new('987654') }) + r2 = Response.new(true, '2', {}) + m = MultiResponse.run do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => '1234567', 'message' => nil, 'street_match' => nil, 'postal_match' => nil } + assert_equal m.cvv_result, { 'code' => '987654', 'message' => nil } + end + + def test_handles_response_without_avs_and_cvv_result + r1 = Response.new(true, '1', {}) + r2 = Response.new(true, '2', {}) + m = MultiResponse.run do |r| + r.process { r1 } + r.process { r2 } + end + assert_equal [r1, r2], m.responses + assert_equal m.avs_result, { 'code' => nil, 'message' => nil, 'street_match' => nil, 'postal_match' => nil } + assert_equal m.cvv_result, { 'code' => nil, 'message' => nil } + end + + def test_handles_responses_avs_and_cvv_result_with_no_responses_provideds + m = MultiResponse.new + assert_equal m.avs_result, nil + assert_equal m.cvv_result, nil + end end diff --git a/test/unit/transcripts/alelo_purchase b/test/unit/transcripts/alelo_purchase new file mode 100644 index 00000000000..6cd426f43a4 --- /dev/null +++ b/test/unit/transcripts/alelo_purchase @@ -0,0 +1,69 @@ +<- "POST /alelo/sandbox/captura-oauth-provider/oauth/token HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox-api.alelo.com.br\r\nContent-Length: 158\r\n\r\n" +<- "grant_type=client_credentials&client_id=ed702c5c-117c-4bc6-989a-055ede547b6d&client_secret=uI3cG0nO5cI0lQ4mY2aF1eN4kV0jA4vC1bJ1rJ0gV2pW7aU4uC&scope=%2Fcapture" +-> "HTTP/1.1 200 OK\r\n" +-> "X-Backside-Transport: FAIL FAIL\r\n" +-> "Connection: close\r\n" +-> "Transfer-Encoding: chunked\r\n" +-> "Content-Type: application/json\r\n" +-> "Cache-Control: private, no-store, no-cache, must-revalidate\r\n" +-> "Pragma: no-cache\r\n" +-> "Content-Security-Policy: default-src 'self'; style-src 'unsafe-inline'\r\n" +-> "Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID\r\n" +-> "Access-Control-Allow-Origin: *\r\n" +-> "Access-Control-Allow-Methods: POST\r\n" +-> "Content-Encoding: gzip\r\n" +-> "\r\n" +-> xxxxxx some binary string xxxxx +-> Decoded Response Body: +-> { "token_type":"Bearer", "access_token":"AAIkODFiYjJlYWYtM2Q1OS00OTM5LTk4ZTctOTRiZjllZTQ2MzljYzd8XKy3u3vEEWIl6xiQGwMiCSkebtYZzHEZX8N8h4pXS1RtqPrxc9Vz6KszCIITgA0tiGkfjj1yl4n3Z9F4-zic5Va0EvvbHLCBzYiQJCmE5ezh9d_1I5I4ncDnKZa5", "expires_in":86400, "consented_on":1666785813, "scope":"/capture" }opening connection to api.alelo.com.br:443... +starting SSL for sandbox-api.alelo.com.br:443... +SSL established, protocol: TLSv1.2, cipher: DHE-RSA-AES256-GCM-SHA384 +<- "GET /alelo/sandbox/capture/key?format=json HTTP/1.1\r\nAccept: application/json\r\nX-Ibm-Client-Id: ed702c5c-117c-4bc6-989a-055ede547b6d\r\nX-Ibm-Client-Secret: uI3cG0nO5cI0lQ4mY2aF1eN4kV0jA4vC1bJ1rJ0gV2pW7aU4uC\r\nAuthorization: Bearer AAIkZWQ3MDJjNWMtMTE3Yy00YmM2LTk4OWEtMDU1ZWRlNTQ3YjZkT5y20AlJqCMxbP0m6e7NnLCghHw7E50NsrAopbwLbtZ7zbDeSVOfVdxkIMbT6XnQrOAN65uFJ_ZH9FLh4dIUV9KOzJyBYnf4YND493nATHFhQpepNvo41qu7rykjBkEw\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox-api.alelo.com.br\r\n\r\n" +-> "HTTP/1.1 200 OK\r\n" +-> "X-Backside-Transport: OK OK\r\n" +-> "Connection: close\r\n" +-> "Transfer-Encoding: chunked\r\n" +-> "Content-Type: application/json\r\n" +-> "Date: Wed, 24 Aug 2022 22:20:34 GMT\r\n" +-> "X-Global-Transaction-ID: 379298106306a42155f1a5f2\r\n" +-> "x-content-type-options: nosniff\r\n" +-> "x-xss-protection: 1; mode=block\r\n" +-> "cache-control: no-cache, no-store, max-age=0, must-revalidate\r\n" +-> "pragma: no-cache\r\n" +-> "expires: 0\r\n" +-> "x-frame-options: DENY\r\n" +-> "Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID\r\n" +-> "Access-Control-Allow-Origin: *\r\n" +-> "Access-Control-Allow-Methods: GET\r\n" +-> "Set-Cookie: 20af66d9029740e40c713b05f443f86d=2bdc6d7099faf31789e088a0d77ea5cc; path=/; HttpOnly; Secure; SameSite=None\r\n" +-> "X-RateLimit-Limit: name=rate-limit,400;\r\n" +-> "X-RateLimit-Remaining: name=rate-limit,399;\r\n" +-> "Content-Encoding: gzip\r\n" +-> xxxxxx some binary string xxxxx +-> Decoded Response Body: +-> {"uuid":"81346b5b-7ce0-4e89-b049-5488b7c8a2b6","format":"BASE64","publicKey":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnWPNWBkwWKDzJmRxxue2fCTrTSMJAoPNn32fIhbw9rNO5GErDOKg5rWzGcIthcH9F2BKIOVZ9YAc0kRVZlsSef2anOxwhFsA6gvj4H4R6lkb8lM9ee9YKw+MrWebKvj78g7o/z7roQkzS6iGcV4/rg4jL819TAz9kHY3Vf2tJi5wW3lPoXawUpvBXGLU1ZPe1RGekFtzBHyFNWniiY7pXF2qRFdeGJ4vcVxfIcYEAV/Pz9vUyLCsscRVBxA24sgYKt8giIp2Ymr2tpsjNwI+bj3jhsej+vdI/CG5klooLW84MSn6LEcCpaG71OB9KN5dDsZ3yKCCLuhmljEX/Wza8QIDAQAB"}opening connection to api.alelo.com.br:443... +<- "POST /alelo/sandbox/capture/transaction HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nX-Ibm-Client-Id: ed702c5c-117c-4bc6-989a-055ede547b6d\r\nX-Ibm-Client-Secret: uI3cG0nO5cI0lQ4mY2aF1eN4kV0jA4vC1bJ1rJ0gV2pW7aU4uC\r\nAuthorization: Bearer AAIkZWQ3MDJjNWMtMTE3Yy00YmM2LTk4OWEtMDU1ZWRlNTQ3YjZkkyzyfNOK8fm61YM_NgGTTp09udTkhoeCdjHA7nMfXwCbTaiU3BJ6NwNkLqJ7ogfDG2cOhwnhVOmdCQ4wwLRwChUZJ__RHzxbt-MlhtQbGqUkF0i1ZwlNUrO7ElCXsyW0\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox-api.alelo.com.br\r\nContent-Length: 913\r\n\r\n" +<- "{\"token\":\"eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.Sy8e0y387LVWcv0P7nwKtu7DHKWww4fCnCx68OnjhmM9sbhmZ9FTQwznGfnSljWFYxXmQPy2PnvtsdvMsEPp5jUANMKTpp4aAGfS_1x31UBuMeRQT5NMlFG8lhCwbPmNtrEOyB2fZUQBsmePpTtdzzAn1tj_hY7XC2bqGkPJ2zS6K2jXqdtkGWeLSMjEDRdyjDuQ_ybFx6uHfYcN_ioXcetUU_MJ1Ai3snfBU-150fCKTmY0SJ09tWMLCyYmvL416L_ha2UmY3tZm1zvpkyRQ612t88ZCEeUK-ZXBYp7FJT-KAAogG49G-Nadj3PSe8jQdxoIZTP46knT3vYp6OmxQ._5Y3c8IjGVJEpkvk4rXwNA.H7LHUpASrZB7zf4Wj5og2l7OBIvgLb2zEdpEC2NVcQPW612oS5jMnUucd58NGDoNoQssxfpjJXse5K-2V7KYEkRlYoVN39gqrIYNesnqPqTmU2VvpQsarjPVO62DgOes3R-qAKkytR3uB3VqNdYmgzdhWf-5tKc8XwLRa41kIsWo6cL7KvKzNUNS_a083X5IUje7Gh4yuH21RzAYVH3diuWDX9QcrdFMZ0BQxE1SpddG8QBcyGgQGvMsfju3Q2kEDrXuJZUSURRiOHdGOpHcYaTjHiGv-Q-NNVNtlwcMhGguMW93_YG8m5gI8VyW8Nq_2epq9YqZwK2YC7XO9CAMxQCaak1ol3ZqR5eo_RO6_ZIUe5NY6NjSWCLqijrAnARbpBUnaXY8CJN4_xRpg3zgqg.gFksDAHH--hQMyWZtXyOfQ\",\"uuid\":\"53141521-afc8-4a08-af0c-f0382aef43c1\"}" +-> "HTTP/1.1 200 OK\r\n" +-> "X-Backside-Transport: OK OK\r\n" +-> "Connection: close\r\n" +-> "Transfer-Encoding: chunked\r\n" +-> "Content-Type: application/json\r\n" +-> "Date: Wed, 24 Aug 2022 22:20:37 GMT\r\n" +-> "X-Global-Transaction-ID: 379298106306a42455f1a6b2\r\n" +-> "x-content-type-options: nosniff\r\n" +-> "x-xss-protection: 1; mode=block\r\n" +-> "cache-control: no-cache, no-store, max-age=0, must-revalidate\r\n" +-> "pragma: no-cache\r\n" +-> "expires: 0\r\n" +-> "x-frame-options: DENY\r\n" +-> "Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID\r\n" +-> "Access-Control-Allow-Origin: *\r\n" +-> "Access-Control-Allow-Methods: POST\r\n" +-> "Set-Cookie: 20af66d9029740e40c713b05f443f86d=2bdc6d7099faf31789e088a0d77ea5cc; path=/; HttpOnly; Secure; SameSite=None\r\n" +-> "X-RateLimit-Limit: name=rate-limit,400;\r\n" +-> "X-RateLimit-Remaining: name=rate-limit,399;\r\n" +-> "Content-Encoding: gzip\r\n" +-> "\r\n" +-> xxxxxx some binary string xxxxx +Conn close diff --git a/test/unit/transcripts/alelo_purchase_scrubbed b/test/unit/transcripts/alelo_purchase_scrubbed new file mode 100644 index 00000000000..52290d93893 --- /dev/null +++ b/test/unit/transcripts/alelo_purchase_scrubbed @@ -0,0 +1,69 @@ +<- "POST /alelo/sandbox/captura-oauth-provider/oauth/token HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox-api.alelo.com.br\r\nContent-Length: 158\r\n\r\n" +<- "grant_type=client_credentials&client_id=[FILTERED]&client_secret=[FILTERED]&scope=%2Fcapture" +-> "HTTP/1.1 200 OK\r\n" +-> "X-Backside-Transport: FAIL FAIL\r\n" +-> "Connection: close\r\n" +-> "Transfer-Encoding: chunked\r\n" +-> "Content-Type: application/json\r\n" +-> "Cache-Control: private, no-store, no-cache, must-revalidate\r\n" +-> "Pragma: no-cache\r\n" +-> "Content-Security-Policy: default-src 'self'; style-src 'unsafe-inline'\r\n" +-> "Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID\r\n" +-> "Access-Control-Allow-Origin: *\r\n" +-> "Access-Control-Allow-Methods: POST\r\n" +-> "Content-Encoding: gzip\r\n" +-> "\r\n" +-> xxxxxx some binary string xxxxx +-> Decoded Response Body: +-> { "token_type":"Bearer", "access_token":"[FILTERED]", "expires_in":86400, "consented_on":1666785813, "scope":"/capture" }opening connection to api.alelo.com.br:443... +starting SSL for sandbox-api.alelo.com.br:443... +SSL established, protocol: TLSv1.2, cipher: DHE-RSA-AES256-GCM-SHA384 +<- "GET /alelo/sandbox/capture/key?format=json HTTP/1.1\r\nAccept: application/json\r\nX-Ibm-Client-Id:[FILTERED]\r\nX-Ibm-Client-Secret:[FILTERED]\r\nAuthorization: Bearer [FILTERED]\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox-api.alelo.com.br\r\n\r\n" +-> "HTTP/1.1 200 OK\r\n" +-> "X-Backside-Transport: OK OK\r\n" +-> "Connection: close\r\n" +-> "Transfer-Encoding: chunked\r\n" +-> "Content-Type: application/json\r\n" +-> "Date: Wed, 24 Aug 2022 22:20:34 GMT\r\n" +-> "X-Global-Transaction-ID: 379298106306a42155f1a5f2\r\n" +-> "x-content-type-options: nosniff\r\n" +-> "x-xss-protection: 1; mode=block\r\n" +-> "cache-control: no-cache, no-store, max-age=0, must-revalidate\r\n" +-> "pragma: no-cache\r\n" +-> "expires: 0\r\n" +-> "x-frame-options: DENY\r\n" +-> "Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID\r\n" +-> "Access-Control-Allow-Origin: *\r\n" +-> "Access-Control-Allow-Methods: GET\r\n" +-> "Set-Cookie: 20af66d9029740e40c713b05f443f86d=2bdc6d7099faf31789e088a0d77ea5cc; path=/; HttpOnly; Secure; SameSite=None\r\n" +-> "X-RateLimit-Limit: name=rate-limit,400;\r\n" +-> "X-RateLimit-Remaining: name=rate-limit,399;\r\n" +-> "Content-Encoding: gzip\r\n" +-> xxxxxx some binary string xxxxx +-> Decoded Response Body: +-> {"uuid":"81346b5b-7ce0-4e89-b049-5488b7c8a2b6","format":"BASE64","publicKey":"[FILTERED]"}opening connection to api.alelo.com.br:443... +<- "POST /alelo/sandbox/capture/transaction HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nX-Ibm-Client-Id:[FILTERED]\r\nX-Ibm-Client-Secret:[FILTERED]\r\nAuthorization: Bearer [FILTERED]\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: sandbox-api.alelo.com.br\r\nContent-Length: 913\r\n\r\n" +<- "{\"token\":\"eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.Sy8e0y387LVWcv0P7nwKtu7DHKWww4fCnCx68OnjhmM9sbhmZ9FTQwznGfnSljWFYxXmQPy2PnvtsdvMsEPp5jUANMKTpp4aAGfS_1x31UBuMeRQT5NMlFG8lhCwbPmNtrEOyB2fZUQBsmePpTtdzzAn1tj_hY7XC2bqGkPJ2zS6K2jXqdtkGWeLSMjEDRdyjDuQ_ybFx6uHfYcN_ioXcetUU_MJ1Ai3snfBU-150fCKTmY0SJ09tWMLCyYmvL416L_ha2UmY3tZm1zvpkyRQ612t88ZCEeUK-ZXBYp7FJT-KAAogG49G-Nadj3PSe8jQdxoIZTP46knT3vYp6OmxQ._5Y3c8IjGVJEpkvk4rXwNA.H7LHUpASrZB7zf4Wj5og2l7OBIvgLb2zEdpEC2NVcQPW612oS5jMnUucd58NGDoNoQssxfpjJXse5K-2V7KYEkRlYoVN39gqrIYNesnqPqTmU2VvpQsarjPVO62DgOes3R-qAKkytR3uB3VqNdYmgzdhWf-5tKc8XwLRa41kIsWo6cL7KvKzNUNS_a083X5IUje7Gh4yuH21RzAYVH3diuWDX9QcrdFMZ0BQxE1SpddG8QBcyGgQGvMsfju3Q2kEDrXuJZUSURRiOHdGOpHcYaTjHiGv-Q-NNVNtlwcMhGguMW93_YG8m5gI8VyW8Nq_2epq9YqZwK2YC7XO9CAMxQCaak1ol3ZqR5eo_RO6_ZIUe5NY6NjSWCLqijrAnARbpBUnaXY8CJN4_xRpg3zgqg.gFksDAHH--hQMyWZtXyOfQ\",\"uuid\":\"53141521-afc8-4a08-af0c-f0382aef43c1\"}" +-> "HTTP/1.1 200 OK\r\n" +-> "X-Backside-Transport: OK OK\r\n" +-> "Connection: close\r\n" +-> "Transfer-Encoding: chunked\r\n" +-> "Content-Type: application/json\r\n" +-> "Date: Wed, 24 Aug 2022 22:20:37 GMT\r\n" +-> "X-Global-Transaction-ID: 379298106306a42455f1a6b2\r\n" +-> "x-content-type-options: nosniff\r\n" +-> "x-xss-protection: 1; mode=block\r\n" +-> "cache-control: no-cache, no-store, max-age=0, must-revalidate\r\n" +-> "pragma: no-cache\r\n" +-> "expires: 0\r\n" +-> "x-frame-options: DENY\r\n" +-> "Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID\r\n" +-> "Access-Control-Allow-Origin: *\r\n" +-> "Access-Control-Allow-Methods: POST\r\n" +-> "Set-Cookie: 20af66d9029740e40c713b05f443f86d=2bdc6d7099faf31789e088a0d77ea5cc; path=/; HttpOnly; Secure; SameSite=None\r\n" +-> "X-RateLimit-Limit: name=rate-limit,400;\r\n" +-> "X-RateLimit-Remaining: name=rate-limit,399;\r\n" +-> "Content-Encoding: gzip\r\n" +-> "\r\n" +-> xxxxxx some binary string xxxxx +Conn close