diff --git a/.github/workflows/ruby-ci.yml b/.github/workflows/ruby-ci.yml index adc9677b38b..6a208f2f7f5 100644 --- a/.github/workflows/ruby-ci.yml +++ b/.github/workflows/ruby-ci.yml @@ -17,9 +17,7 @@ jobs: strategy: matrix: version: - - 2.5 - - 2.6 - - 2.7 + - 3.1 gemfile: - gemfiles/Gemfile.rails50 - gemfiles/Gemfile.rails51 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000000..5ad2e57628a --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,19 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v8 + with: + stale-issue-message: 'To provide a cleaner slate for the maintenance of the library, this PR/Issue is being labeled stale after 60 days without activity. It will be closed in 14 days unless you comment with an update regarding its applicability to the current build. Thank you!' + stale-pr-message: 'To provide a cleaner slate for the maintenance of the library, this PR/Issue is being labeled stale after 60 days without activity. It will be closed in 14 days unless you comment with an update regarding its applicability to the current build. Thank you!' + days-before-close: 14 + exempt-draft-pr: true \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml index 50d63c3dd84..d8f742f981f 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: 3.1 # Active Merchant gateways are not amenable to length restrictions Metrics/ClassLength: @@ -24,7 +24,7 @@ Metrics/ClassLength: Metrics/ModuleLength: Enabled: false -Layout/AlignParameters: +Layout/ParameterAlignment: EnforcedStyle: with_fixed_indentation Layout/DotPosition: @@ -33,10 +33,96 @@ Layout/DotPosition: Layout/CaseIndentation: EnforcedStyle: end -Layout/IndentHash: +Layout/FirstHashElementIndentation: EnforcedStyle: consistent Naming/PredicateName: Exclude: - "lib/active_merchant/billing/gateways/payeezy.rb" - 'lib/active_merchant/billing/gateways/airwallex.rb' + +Gemspec/DateAssignment: # (new in 1.10) + Enabled: true +Layout/SpaceBeforeBrackets: # (new in 1.7) + Enabled: true +Lint/AmbiguousAssignment: # (new in 1.7) + Enabled: true +Lint/DeprecatedConstants: # (new in 1.8) + Enabled: true # update later in next Update Rubocop PR +Lint/DuplicateBranch: # (new in 1.3) + Enabled: false +Lint/DuplicateRegexpCharacterClassElement: # (new in 1.1) + Enabled: true +Lint/EmptyBlock: # (new in 1.1) + Enabled: false # update later in next Update Rubocop PR + Exclude: + - 'lib/active_merchant/billing/gateways/authorize_net.rb' + - 'lib/active_merchant/billing/gateways/secure_net.rb' +Lint/EmptyClass: # (new in 1.3) + Enabled: true +Lint/FloatComparison: + Exclude: + - 'lib/active_merchant/billing/gateways/payu_latam.rb' +Lint/LambdaWithoutLiteralBlock: # (new in 1.8) + Enabled: true +Lint/NonDeterministicRequireOrder: + Exclude: + - 'script/generate' +Lint/NoReturnInBeginEndBlocks: # (new in 1.2) + Enabled: true + Exclude: + - 'lib/active_merchant/billing/gateways/fat_zebra.rb' + - 'lib/active_merchant/billing/gateways/netbanx.rb' + - 'lib/active_merchant/billing/gateways/payway_dot_com.rb' +Lint/NumberedParameterAssignment: # (new in 1.9) + Enabled: true +Lint/OrAssignmentToConstant: # (new in 1.9) + Enabled: true +Lint/RedundantDirGlobSort: # (new in 1.8) + Enabled: true +Lint/SymbolConversion: # (new in 1.9) + Enabled: true +Lint/ToEnumArguments: # (new in 1.1) + Enabled: true +Lint/TripleQuotes: # (new in 1.9) + Enabled: true +Lint/UnexpectedBlockArity: # (new in 1.5) + Enabled: true +Lint/UnmodifiedReduceAccumulator: # (new in 1.1) + Enabled: true +Style/ArgumentsForwarding: # (new in 1.1) + Enabled: true +Style/CollectionCompact: # (new in 1.2) + Enabled: false # update later in next Update Rubocop PR +Style/DocumentDynamicEvalDefinition: # (new in 1.1) + Enabled: true + Exclude: + - 'lib/active_merchant/billing/credit_card.rb' + - 'lib/active_merchant/billing/response.rb' +Style/EndlessMethod: # (new in 1.8) + Enabled: true +Style/HashConversion: # (new in 1.10) + Enabled: true + Exclude: + - 'lib/active_merchant/billing/gateways/payscout.rb' + - 'lib/active_merchant/billing/gateways/pac_net_raven.rb' +Style/HashExcept: # (new in 1.7) + Enabled: true +Style/IfWithBooleanLiteralBranches: # (new in 1.9) + Enabled: false # update later in next Update Rubocop PR +Style/NegatedIfElseCondition: # (new in 1.2) + Enabled: true +Style/NilLambda: # (new in 1.3) + Enabled: true +Style/RedundantArgument: # (new in 1.4) + Enabled: false # update later in next Update Rubocop PR +Style/StringChars: # (new in 1.12) + Enabled: false # update later in next Update Rubocop PR +Style/SwapValues: # (new in 1.1) + Enabled: true +Naming/VariableNumber: + Enabled: false +Style/OptionalBooleanParameter: + Enabled: false +Style/RedundantRegexpEscape: + Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 359bc075fb3..a9338fe8526 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -12,7 +12,7 @@ # SupportedHashRocketStyles: key, separator, table # SupportedColonStyles: key, separator, table # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit -Layout/AlignHash: +Layout/HashAlignment: Enabled: false # Offense count: 150 @@ -26,7 +26,7 @@ Lint/FormatParameterMismatch: - 'test/unit/credit_card_formatting_test.rb' # Offense count: 2 -Lint/HandleExceptions: +Lint/SuppressedException: Exclude: - 'lib/active_merchant/billing/gateways/mastercard.rb' - 'lib/active_merchant/billing/gateways/trust_commerce.rb' @@ -65,6 +65,8 @@ Metrics/CyclomaticComplexity: # Configuration parameters: CountComments, ExcludedMethods. Metrics/MethodLength: Max: 163 + IgnoredMethods: + - 'setup' # Offense count: 2 # Configuration parameters: CountKeywordArgs. @@ -99,7 +101,7 @@ Naming/MethodName: # Offense count: 14 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. # AllowedNames: io, id, to, by, on, in, at, ip, db -Naming/UncommunicativeMethodParamName: +Naming/MethodParameterName: Exclude: - 'lib/active_merchant/billing/gateways/blue_snap.rb' - 'lib/active_merchant/billing/gateways/cyber_source.rb' @@ -173,13 +175,6 @@ Style/BarePercentLiterals: Style/BlockDelimiters: Enabled: false -# Offense count: 440 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: braces, no_braces, context_dependent -Style/BracesAroundHashParameters: - Enabled: false - # Offense count: 2 Style/CaseEquality: Exclude: @@ -231,10 +226,34 @@ Style/ColonMethodCall: # Configuration parameters: Keywords. # Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW Style/CommentAnnotation: - Exclude: - - 'test/remote/gateways/remote_usa_epay_advanced_test.rb' - - 'test/unit/gateways/authorize_net_cim_test.rb' - - 'test/unit/gateways/usa_epay_advanced_test.rb' + Enabled: false # update later in next Update Rubocop PR + +Style/StringConcatenation: + Enabled: false # update later in next Update Rubocop PR +Style/SingleArgumentDig: + Enabled: false # update later in next Update Rubocop PR +Style/SlicingWithRange: + Enabled: false # update later in next Update Rubocop PR +Style/HashEachMethods: + Enabled: false # update later in next Update Rubocop PR +Style/CaseLikeIf: + Enabled: false # update later in next Update Rubocop PR +Style/HashLikeCase: + Enabled: false # update later in next Update Rubocop PR +Style/GlobalStdStream: + Enabled: false # update later in next Update Rubocop PR +Style/HashTransformKeys: + Enabled: false # update later in next Update Rubocop PR +Style/HashTransformValues: + Enabled: false # update later in next Update Rubocop PR +Lint/RedundantSafeNavigation: + Enabled: false # update later in next Update Rubocop PR +Lint/EmptyConditionalBody: + Enabled: false # update later in next Update Rubocop PR +Style/SoleNestedConditional: + Exclude: # update later in next Update Rubocop PR + - 'lib/active_merchant/billing/gateways/card_connect.rb' + - 'lib/active_merchant/billing/gateways/blue_snap.rb' # Offense count: 8 Style/CommentedKeyword: @@ -381,17 +400,7 @@ Style/FormatString: # Configuration parameters: EnforcedStyle. # SupportedStyles: annotated, template, unannotated Style/FormatStringToken: - Exclude: - - 'lib/active_merchant/billing/gateways/redsys.rb' - - 'lib/active_merchant/connection.rb' - - 'lib/active_merchant/network_connection_retries.rb' - - 'test/remote/gateways/remote_balanced_test.rb' - - 'test/remote/gateways/remote_openpay_test.rb' - - 'test/unit/gateways/balanced_test.rb' - - 'test/unit/gateways/elavon_test.rb' - - 'test/unit/gateways/exact_test.rb' - - 'test/unit/gateways/firstdata_e4_test.rb' - - 'test/unit/gateways/safe_charge_test.rb' + Enabled: false # Offense count: 679 # Cop supports --auto-correct. @@ -410,6 +419,15 @@ Style/GlobalVars: - 'test/unit/gateways/finansbank_test.rb' - 'test/unit/gateways/garanti_test.rb' +Lint/MissingSuper: + Exclude: + - 'lib/active_merchant/billing/gateways/payway.rb' + - 'lib/active_merchant/billing/response.rb' + - 'lib/active_merchant/billing/gateways/orbital/orbital_soft_descriptors.rb' + - 'lib/active_merchant/billing/gateways/orbital.rb' + - 'lib/active_merchant/billing/gateways/linkpoint.rb' + - 'lib/active_merchant/errors.rb' + # Offense count: 196 # Configuration parameters: MinBodyLength. Style/GuardClause: @@ -448,6 +466,7 @@ Style/InverseMethods: Exclude: - 'lib/active_merchant/billing/gateways/ogone.rb' - 'lib/active_merchant/billing/gateways/worldpay.rb' + Enabled: false # update later in next Update Rubocop PR # Offense count: 32 # Cop supports --auto-correct. @@ -733,6 +752,6 @@ Style/ZeroLengthPredicate: # Offense count: 9321 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https -Metrics/LineLength: +Layout/LineLength: Max: 2602 diff --git a/CHANGELOG b/CHANGELOG index fdbf33a2c50..38c8836c43d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,279 @@ = ActiveMerchant CHANGELOG == HEAD +* Bump Ruby version to 3.1 [dustinhaefele] #5104 +* FlexCharge: Update inquire method to use the new orders end-point +* Worldpay: Prefer options for network_transaction_id [aenand] #5129 +* Braintree: Prefer options for network_transaction_id [aenand] #5129 +* Cybersource Rest: Update support for stored credentials [aenand] #5083 +* Plexo: Add support to NetworkToken payments [euribe09] #5130 + +== Version 1.136.0 (June 3, 2024) +* 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 +* Braintree: Create credit card nonce [gasb150] #4897 +* 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 +* SecurionPay/Shift4_v2: authorization from [gasb150] #4913 +* Rapyd: Update recurrence_type field [yunnydang] #4922 +* Element: Add lodging fields [yunnydang] #4813 +* SafeCharge: Update sg_CreditType field on the credit method [yunnydang] #4918 +* Rapyd: add force_3ds_secure flag [Heavyblade] #4927 +* Beanstream: add alternate option for passing phone number [jcreiff] #4923 +* AuthorizeNet: Update network token method [almalee24] #4852 +* Adding Oauth Response for access tokens [almalee24] #4907 +* GlobalCollect: Added support for 3DS exemption request field [almalee24] #4917 +* NMI: Update supported countries list [jcreiff] #4931 +* Adyen: Add mcc field [jcreiff] #4926 +* Quickbooks: Remove raise OAuth from extract_response_body_or_raise [almalee24] #4935 +* Cecabank: Add new Cecabank gateway to use the JSON REST API [sinourain] #4920 +* Cecabank: Add 3DS Global to Cecabank REST JSON gateway [sinourain] #4940 +* Cecabank: Add scrub implementation [sinourain] #4945 +* GlobalCollect: Fix bug in success_from logic [DustinHaefele] #4939 +* Worldpay: Update 3ds logic to accept df_reference_id directly [DustinHaefele] #4929 +* Orbital: Enable Third Party Vaulting [javierpedrozaing] #4928 +* Payeezy: Add the customer_ref and reference_3 fields [yunnydang] #4942 +* Redsys Rest: Add support for new gateway type Redsys Rest [aenand] #4951 +* CyberSource: Surface the reconciliationID2 field [yunnydang] #4934 +* Worldpay: Update stored credentials logic [DustinHaefele] #4950 +* Vantiv Express: New Xml gateway [DustinHaefele] #4956 +* Shift4 V2: Add unstore function [javierpedrozaing] #4953 +* CommerceHub: Add 3DS global support [sinourain] #4957 +* SumUp Gateway: Fix refund method [sinourain] #4924 +* Braintree: Add v2 stored credential option [aenand] #4937 +* Cybersource REST: Remove request-target parens [curiousepic] #4960 +* Ogone: Fix signature calulcation for blank fields [Heavyblade] #4963 +* VisaNet Peru: Add purchaseNumber to response object [yunnydang] #4961 +* SafeCharge: Support tokens [almalee24] #4948 +* Redsys: Update to $0 verify [almalee24] #4944 +* Litle: Update stored credentials [almalee24] #4903 +* WorldPay: Accept GooglePay pan only [almalee24] #4943 +* Braintree: Correct issue in v2 stored credentials [aenand] #4967 +* Stripe Payment Intents: Add the card brand field [yunnydang] #4964 +* Rapyd: Enable new auth mode payment_redirect [javierpedrozaing] #4970 +* Cecabank: Fix exemption_type when it is blank and update the error code for some tests [sinourain] #4968 +* RedsysRest: Update to $0 verify [almalee24] #4973 +* CommerceHub: Add credit transaction [sinourain] #4965 +* PayTrace: Send CSC value on gateway request. [DustinHaefele] #4974 +* Orbital: Remove needless GSF for TPV [javierpedrozaing] #4959 +* Adyen: Provide ZZ as default country code [jcreiff] #4971 +* MIT: Add test_url [jcreiff] #4977 +* VantivExpress: Fix eci bug [almalee24] #4982 +* IPG: Allow for Merchant Aggregator credential usage [DustinHaefele] #4986 +* Adyen: Add support for `metadata` object [rachelkirk] #4987 +* Xpay: New adapter basic operations added [jherreraa] #4669 +* Rapyd: Enable idempotent request support [javierpedrozaing] #4980 +* Litle: Update account type [almalee24] #4976 +* Wompi: Add support for `tip_in_cents` [rachelkirk] #4983 +* HiPay: Add Gateway [gasb150] #4979 +* Xpay: New adapter basic operations added [jherreraa] #4669 +* Braintree: Add support for more payment details fields in response [yunnydang] #4992 +* CyberSource: Add the first_recurring_payment auth service field [yunnydang] #4989 +* CommerceHub: Add dynamic descriptors [jcreiff] #4994 +* Rapyd: Update email mapping [javierpedrozaing] #4996 +* SagePay: Add support for v4 [aenand] #4990 +* Braintree: Send merchant_account_id when generating client token [almalee24] #4991 +* CheckoutV2: Update reponse message for 3DS transactions [almalee24] #4975 +* HiPay: Scrub/Refund/Void [gasb150] #4995 +* Rapyd: Adding fixed_side and requested_currency options [Heavyblade] #4962 +* Add new card type Tuya. GlobalCollect & Decidir: Improve support for Tuya card type [sinourain] #4993 +* Cecabank: Encrypt credit card fields [sinourain] #4998 +* HiPay: Add unstore [gasb150] #4999 +* Rapyd: Fix transaction with two digits in month and year [javierpedrozaing] #5008 +* SagePay: Add support for stored credentials [almalee24] #5007 +* Payeezy: Pull cardholer name from billing address [almalee24] #5006 +* HiPay: Add 3ds params [gasb150] #5012 +* Cecabank: Fix gateway scrub method [sinourain] #5009 +* Pin: Add the platform_adjustment field [yunnydang] #5011 +* Priority: Allow gateway fields to be available on capture [yunnydang] #5010 +* Add payment_data to NetworkTokenizationCreditCard [almalee24] #4888 +* IPG: Update handling of ChargeTotal [jcreiff] #5017 +* Plexo: Add the invoice_number field [yunnydang] #5019 +* CheckoutV2: Handle empty address in payout destination data [jcreiff] #5024 +* CyberSource: Add the auth service aggregator_id field [yunnydang] #5026 +* Cecabank: exclude 3ds empty parameter [jherreraa] #5021 +* Moneris: Add the customer id field [yunnydang] #5028 +* Kushki: Add the product_details field [yunnydang] #5027 +* GlobalCollect: Add support for encryptedPaymentData [almalee24] #5015 +* Rapyd: Adding 500 errors handling [Heavyblade] #5029 +* SumUp: Add 3DS fields [sinourain] #5030 +* Cecabank: Enable network_transaction_id as GSF [javierpedrozaing] #5034 +* Braintree: Surface the paypal_details in response object [yunnydang] #5043 +* Worldline (formerly GlobalCollect): Update API endpoints [deemeyers] #5049 +* Quickbooks: Update scrub method [almalee24] #5049 +* Worldline (formerly GlobalCollect):Remove decrypted payment data [almalee24] #5032 +* StripePI: Update authorization_from [almalee24] #5048 +* FirstPay: Add REST JSON transaction methods [sinourain] #5035 +* Braintree: Add payment details to failed transaction hash [yunnydang] #5050 +* Cecabank: Amex CVV Update [sinourain] #5051 +* FirstPay: Add support for ApplePay and GooglePay [sinourain] #5036 +* XPay: Update 3DS to support 3 step process [sinourain] #5046 +* SagePay: Update API endpoints [almalee24] #5057 +* Bin Update: Add sodexo bins [yunnydang] #5061 +* Authorize Net: Add the surcharge field [yunnydang] #5062 +* Paymentez: Update field for reference_id [almalee24] #5065 +* CyberSource: Extend support for `gratuity_amount` and update Mastercard NT field order [rachelkirk] #5063 +* XPay: Refactor basic transactions after implement 3DS 3steps API [sinourain] #5058 +* AuthorizeNet: Remove turn_on_nt flow [almalee24] #5056 +* CheckoutV2: Add processing and recipient fields [yunnydang] #5068 +* RedsysRest: Omit CVV from requests when not present [jcreiff] #5077 +* Bin Update: Add Unionpay bin [yunnydang] #5079 +* MerchantWarrior: Adding support for 3DS Global fields [Heavyblade] #5072 +* FatZebra: Adding third-party 3DS params [Heavyblade] #5066 +* SumUp: Remove Void method [sinourain] #5060 +* StripePI: Add new ApplePay and GooglePay flow [almalee24] #5075 +* Braintree: Add merchant_account_id to Verify [almalee24] #5070 +* Paymentez: Update success_from [jherrera] #5082 +* Update Rubocop to 1.14.0 [almalee24] #5069 +* Adyen: Update error code mapping [dustinhaefele] #5085 +* Updates to StripePI scrub and Paymentez success_from [almalee24] #5090 +* Bin Update: Add Routex bin [yunnydang] #5089 +* SumUp: Improve success_from and message_from methods [sinourain] #5087 +* Adyen: Send new ignore_threed_dynamic for success_from [almalee24] #5078 +* Plexo: Add flow field to capture, purchase, and auth [yunnydang] #5092 +* PayTrace: Always send name in billing_address [almalee24] #5086 +* StripePI: Update eci format [almalee24] #5097 +* Paymentez: Remove reference_id flag [almalee24] #5081 +* Cybersource Rest: Add support for normalized three ds [aenand] #5105 +* Braintree: Add additional data to response [aenand] #5084 +* CheckoutV2: Retain and refresh OAuth access token [sinourain] #5098 +* Worldpay: Remove default ECI value [aenand] #5103 +* DataTrans: Add Gateway [gasb150] #5108 +* CyberSource: Update NT flow [almalee24] #5106 +* FlexCharge: Add Gateway [Heavyblade] #5108 +* Litle: Update enhanced data fields to pass integers [yunnydang] #5113 +* Litle: Update commodity code and line item total fields [yunnydang] #5115 +* Cybersource Rest: Add support for network tokens [aenand] #5107 +* Decidir: Add support for customer object [rachelkirk] #5071 +* Worldpay: Add support for stored credentials with network tokens [aenand] #5114 +* Paymentez: Update success_from method for refunds [almalee24] #5116 +* DataTrans: Add ThirdParty 3DS params [gasb150] #5118 +* FlexCharge: Add ThirdParty 3DS params [javierpedrozaing] #5121 +* FlexCharge: Add support for TPV store [edgarv09] #5120 +* CheckoutV2: Add sender payment fields to purchase and auth [yunnydang] #5124 +* HiPay: Fix parse authorization string [javierpedrozaing] #5119 +* Worldpay: Add support for deafult ECI value [aenand] #5126 +* DLocal: Update stored credentials [sinourain] #5112 +* NMI: Add NTID override [yunnydang] #5134 +* Cybersource Rest: Support L2/L3 data [aenand] #5117 +* Worldpay: Support L2/L3 data [aenand] #5117 +* Support UATP cardtype [javierpedrozaing] #5137 +* Litle: Add 141 and 142 as successful responses [almalee24] #5135 + +== 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 +* Quickbooks: Remove OAuth response from refresh_access_token [almalee24] #4949 + +== 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 @@ -55,6 +328,47 @@ * 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 @@ -706,6 +1020,7 @@ * PayU Latam: Improve error response [esmitperez] #3717 * Vantiv: Vantiv Express - CardPresentCode, PaymentType, SubmissionType, DuplicateCheckDisableFlag [esmitperez] #3730,#3731 * Cybersource: Ensure issueradditionaldata comes before partnerSolutionId [britth] #3733 +* RedsysRest: Add support for 3DS [almalee24] #5042 == Version 1.111.0 * Fat Zebra: standardized 3DS fields and card on file extra data for Visa scheme rules [montdidier] #3409 diff --git a/Gemfile b/Gemfile index 174afc778d3..ffe8c804b8d 100644 --- a/Gemfile +++ b/Gemfile @@ -2,12 +2,13 @@ source 'https://rubygems.org' gemspec gem 'jruby-openssl', platforms: :jruby -gem 'rubocop', '~> 0.62.0', require: false +gem 'rubocop', '~> 1.14.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 'jose', '~> 1.1.3' + gem 'braintree', '>= 4.14.0' + gem 'jose', '~> 1.2.0' gem 'jwe' gem 'mechanize' + gem 'timecop' end diff --git a/activemerchant.gemspec b/activemerchant.gemspec index e72702e8afc..78484f81232 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 = '>= 3.1' 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..d9438f7d281 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: ruby: - version: '2.5.0' + version: '3.1.0' dependencies: cache_directories: diff --git a/lib/active_merchant/billing/check.rb b/lib/active_merchant/billing/check.rb index ca4d0171bd7..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 diff --git a/lib/active_merchant/billing/compatibility.rb b/lib/active_merchant/billing/compatibility.rb index 319ec8a4350..fcd14928b40 100644 --- a/lib/active_merchant/billing/compatibility.rb +++ b/lib/active_merchant/billing/compatibility.rb @@ -29,7 +29,7 @@ def self.humanize(lower_case_and_underscored_word) result = lower_case_and_underscored_word.to_s.dup result.gsub!(/_id$/, '') result.tr!('_', ' ') - result.gsub(/([a-z\d]*)/i, &:downcase).gsub(/^\w/) { $&.upcase } + result.gsub(/([a-z\d]*)/i, &:downcase).gsub(/^\w/) { Regexp.last_match(0).upcase } end end end @@ -90,8 +90,8 @@ def add_to_base(error) add(:base, error) end - def each_full - full_messages.each { |msg| yield msg } + def each_full(&block) + full_messages.each(&block) end def full_messages @@ -113,6 +113,6 @@ def full_messages end end - Compatibility::Model.send(:include, Rails::Model) + Compatibility::Model.include Rails::Model end end diff --git a/lib/active_merchant/billing/credit_card.rb b/lib/active_merchant/billing/credit_card.rb index 32460e7785e..95e7ae5ce38 100644 --- a/lib/active_merchant/billing/credit_card.rb +++ b/lib/active_merchant/billing/credit_card.rb @@ -38,6 +38,10 @@ module Billing #:nodoc: # * Edenred # * Anda # * Creditos directos (Tarjeta D) + # * Panal + # * Verve + # * Tuya + # * UATP # # 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 @@ -130,6 +134,10 @@ def number=(value) # * +'edenred'+ # * +'anda'+ # * +'tarjeta-d'+ + # * +'panal'+ + # * +'verve'+ + # * +'tuya'+ + # * +'uatp'+ # # Or, if you wish to test your implementation, +'bogus'+. # @@ -286,7 +294,7 @@ def name=(full_name) end %w(month year start_month start_year).each do |m| - class_eval %( + class_eval <<~RUBY, __FILE__, __LINE__ + 1 def #{m}=(v) @#{m} = case v when "", nil, 0 @@ -295,7 +303,7 @@ def #{m}=(v) v.to_i end end - ) + RUBY end def verification_value? @@ -393,9 +401,7 @@ def validate_essential_attributes #:nodoc: def validate_card_brand_and_number #:nodoc: errors = [] - if !empty?(brand) - errors << [:brand, 'is invalid'] if !CreditCard.card_companies.include?(brand) - end + errors << [:brand, 'is invalid'] if !empty?(brand) && !CreditCard.card_companies.include?(brand) if empty?(number) errors << [:number, 'is required'] @@ -403,9 +409,7 @@ def validate_card_brand_and_number #:nodoc: errors << [:number, 'is not a valid credit card number'] end - if errors.empty? - errors << [:brand, 'does not match the card number'] if !CreditCard.matching_brand?(number, brand) - end + errors << [:brand, 'does not match the card number'] if errors.empty? && !CreditCard.matching_brand?(number, brand) errors end @@ -423,6 +427,7 @@ def validate_verification_value #:nodoc: class ExpiryDate #:nodoc: attr_reader :month, :year + def initialize(month, year) @month = month.to_i @year = year.to_i diff --git a/lib/active_merchant/billing/credit_card_formatting.rb b/lib/active_merchant/billing/credit_card_formatting.rb index ef8a6894ba6..d91d1dba38a 100644 --- a/lib/active_merchant/billing/credit_card_formatting.rb +++ b/lib/active_merchant/billing/credit_card_formatting.rb @@ -5,6 +5,10 @@ def expdate(credit_card) "#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}" end + def strftime_yyyymm(credit_card) + format(credit_card.year, :four_digits) + format(credit_card.month, :two_digits) + end + # This method is used to format numerical information pertaining to credit cards. # # format(2005, :two_digits) # => "05" diff --git a/lib/active_merchant/billing/credit_card_methods.rb b/lib/active_merchant/billing/credit_card_methods.rb index 154f06b2556..82508247f4c 100644 --- a/lib/active_merchant/billing/credit_card_methods.rb +++ b/lib/active_merchant/billing/credit_card_methods.rb @@ -24,7 +24,11 @@ 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|505864|505865)\d{10}$/ }, + 'sodexo' => lambda { |num| + num&.size == 16 && ( + SODEXO_BINS.any? { |bin| num.slice(0, bin.size) == bin } + ) + }, 'alia' => ->(num) { num =~ /^(504997|505878|601030|601073|505874)\d{10}$/ }, 'vr' => ->(num) { num =~ /^(627416|637036)\d{10}$/ }, 'unionpay' => ->(num) { (16..19).cover?(num&.size) && in_bin_range?(num.slice(0, 8), UNIONPAY_RANGES) }, @@ -39,13 +43,18 @@ 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 =~ /^(700674|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}$/ } + '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) }, + 'tuya' => ->(num) { num =~ /^588800\d{10}$/ }, + 'uatp' => ->(num) { num =~ /^(1175|1290)\d{11}$/ } } SODEXO_NO_LUHN = ->(num) { num =~ /^(505864|505865)\d{10}$/ } @@ -70,6 +79,13 @@ module CreditCardMethods (491730..491759), ] + SODEXO_BINS = Set.new( + %w[ + 606071 603389 606070 606069 606068 600818 505864 505865 + 60607601 60607607 60894400 60894410 60894420 60607606 + ] + ) + CARNET_RANGES = [ (506199..506499), ] @@ -108,7 +124,7 @@ module CreditCardMethods MAESTRO_BINS = Set.new( %w[ 500057 501018 501043 501045 501047 501049 501051 501072 501075 501083 501087 501089 501095 - 501500 + 501500 501623 501879 502113 502120 502121 502301 503175 503337 503645 503670 504310 504338 504363 504533 504587 504620 504639 504656 504738 504781 504910 @@ -181,7 +197,8 @@ module CreditCardMethods (601256..601276), (601640..601652), (601689..601700), - (602011..602050), + (602011..602048), + [602050], (630400..630499), (639000..639099), (670000..679999), @@ -193,10 +210,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. @@ -218,7 +235,8 @@ module CreditCardMethods 58965700..58965799, 60352200..60352299, 65027200..65027299, - 65008700..65008700 + 65008700..65008700, + 65090000..65090099 ] MADA_RANGES = [ @@ -233,7 +251,7 @@ module CreditCardMethods # https://www.discoverglobalnetwork.com/content/dam/discover/en_us/dgn/pdfs/IPP-VAR-Enabler-Compliance.pdf UNIONPAY_RANGES = [ - 62000000..62000000, 62212600..62379699, 62400000..62699999, 62820000..62889999, + 62000000..62000000, 62178570..62178570, 62212600..62379699, 62400000..62699999, 62820000..62889999, 81000000..81099999, 81100000..81319999, 81320000..81519999, 81520000..81639999, 81640000..81719999 ] @@ -241,6 +259,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 @@ -404,7 +466,7 @@ def valid_by_algorithm?(brand, numbers) #:nodoc: valid_naranja_algo?(numbers) when 'creditel' valid_creditel_algo?(numbers) - when 'alia', 'confiable', 'maestro_no_luhn', 'anda', 'tarjeta-d' + when 'alia', 'confiable', 'maestro_no_luhn', 'anda', 'tarjeta-d', 'hipercard' true when 'sodexo' sodexo_no_luhn?(numbers) ? true : valid_luhn?(numbers) 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 ccc10ba6c51..465be06170b 100644 --- a/lib/active_merchant/billing/gateways/adyen.rb +++ b/lib/active_merchant/billing/gateways/adyen.rb @@ -68,6 +68,9 @@ def authorize(money, payment, 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) + add_metadata(post, options) commit('authorise', post, options) end @@ -77,6 +80,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 @@ -90,12 +94,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 @@ -222,17 +242,35 @@ 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] + post[:mcc] = options[:mcc] if options[:mcc] + + 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] @@ -241,11 +279,7 @@ 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 extract_and_transform(mapper, from) @@ -290,13 +324,90 @@ def add_level_3_data(post, options) post[:additionalData].compact! 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 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) @@ -314,7 +425,7 @@ def add_merchant_data(post, options) def add_risk_data(post, options) if (risk_data = options[:risk_data]) - risk_data = Hash[risk_data.map { |k, v| ["riskdata.#{k}", v] }] + risk_data = risk_data.map { |k, v| ["riskdata.#{k}", v] }.to_h post[:additionalData].merge!(risk_data) end end @@ -384,30 +495,39 @@ 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) - post[:deliveryAddress][:country] = address[:country] if address[:country] + post[:deliveryAddress][:country] = get_country(address) end 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] = get_country(address) + post[:telephoneNumber] = address[:phone_number] || address[:phone] || '' + end + def get_state(address) address[:state] && !address[:state].blank? ? address[:state] : 'NA' end + def get_country(address) + address[:country].present? ? address[:country] : 'ZZ' + end + def add_invoice(post, money, options) currency = options[:currency] || currency(money) amount = { @@ -435,7 +555,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 @@ -468,6 +588,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] @@ -486,7 +617,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 @@ -497,11 +630,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) @@ -597,6 +731,30 @@ 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 add_metadata(post, options = {}) + return unless options[:metadata] + + post[:metadata] ||= {} + post[:metadata].merge!(options[:metadata]) if options[:metadata] + end + def parse(body) return {} if body.blank? @@ -615,7 +773,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?, @@ -635,8 +793,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) @@ -663,7 +827,7 @@ def request_headers(options) end def success_from(action, response, options) - if %w[RedirectShopper ChallengeShopper].include?(response.dig('resultCode')) && !options[:execute_threed] && !options[:threed_dynamic] + if %w[RedirectShopper ChallengeShopper].include?(response.dig('resultCode')) && !options[:execute_threed] && (!options[:threed_dynamic] || options[:ignore_threed_dynamic]) response['refusalReason'] = 'Received unexpected 3DS authentication response, but a 3DS initiation flag was not included in the request.' return false end @@ -680,18 +844,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 - response['response'] || response['message'] || response['result'] || response['resultCode'] + def authorize_message_from(response, options = {}) + return raw_authorize_error_message(response) if options[:raw_error_message] + + 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 @@ -720,7 +903,7 @@ def post_data(action, parameters = {}) end def error_code_from(response) - STANDARD_ERROR_CODE_MAPPING[response['errorCode']] || response['errorCode'] + response.dig('additionalData', 'refusalReasonRaw').try(:scan, /^\d+/).try(:first) || STANDARD_ERROR_CODE_MAPPING[response['errorCode']] || response['errorCode'] || response['refusalReason'] end def network_transaction_id_from(response) diff --git a/lib/active_merchant/billing/gateways/airwallex.rb b/lib/active_merchant/billing/gateways/airwallex.rb index e60f5d8de96..8d81e54999f 100644 --- a/lib/active_merchant/billing/gateways/airwallex.rb +++ b/lib/active_merchant/billing/gateways/airwallex.rb @@ -32,7 +32,7 @@ def initialize(options = {}) @client_id = options[:client_id] @client_api_key = options[:client_api_key] super - @access_token = setup_access_token + @access_token = options[:access_token] || setup_access_token end def purchase(money, card, options = {}) @@ -133,13 +133,27 @@ def setup_access_token 'x-client-id' => @client_id, 'x-api-key' => @client_api_key } - response = ssl_post(build_request_url(:login), nil, token_headers) - JSON.parse(response)['token'] + + begin + raw_response = ssl_post(build_request_url(:login), nil, token_headers) + rescue ResponseError => e + raise OAuthResponseError.new(e) + else + response = JSON.parse(raw_response) + if (token = response['token']) + token + else + oauth_response = Response.new(false, response['message']) + raise OAuthResponseError.new(oauth_response) + end + end end 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) @@ -278,11 +292,11 @@ def add_three_ds(post, options) pm_options = post.dig('payment_method_options', 'card') external_three_ds = { - 'version': format_three_ds_version(three_d_secure), - 'eci': three_d_secure[:eci] + version: format_three_ds_version(three_d_secure), + eci: three_d_secure[:eci] }.merge(three_ds_version_specific_fields(three_d_secure)) - pm_options ? pm_options.merge!('external_three_ds': external_three_ds) : post['payment_method_options'] = { 'card': { 'external_three_ds': external_three_ds } } + pm_options ? pm_options.merge!(external_three_ds: external_three_ds) : post['payment_method_options'] = { card: { external_three_ds: external_three_ds } } end def format_three_ds_version(three_d_secure) @@ -295,14 +309,14 @@ def format_three_ds_version(three_d_secure) def three_ds_version_specific_fields(three_d_secure) if three_d_secure[:version].to_f >= 2 { - 'authentication_value': three_d_secure[:cavv], - 'ds_transaction_id': three_d_secure[:ds_transaction_id], - 'three_ds_server_transaction_id': three_d_secure[:three_ds_server_trans_id] + authentication_value: three_d_secure[:cavv], + ds_transaction_id: three_d_secure[:ds_transaction_id], + three_ds_server_transaction_id: three_d_secure[:three_ds_server_trans_id] } else { - 'cavv': three_d_secure[:cavv], - 'xid': three_d_secure[:xid] + cavv: three_d_secure[:cavv], + xid: three_d_secure[:xid] } end end diff --git a/lib/active_merchant/billing/gateways/alelo.rb b/lib/active_merchant/billing/gateways/alelo.rb index 69086bc77fd..381b5859372 100644 --- a/lib/active_merchant/billing/gateways/alelo.rb +++ b/lib/active_merchant/billing/gateways/alelo.rb @@ -110,8 +110,18 @@ def fetch_access_token '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) + begin + raw_response = ssl_post(url('captura-oauth-provider/oauth/token'), post_data(params), headers) + rescue ResponseError => e + raise OAuthResponseError.new(e) + else + response = parse(raw_response) + if (access_token = response[:access_token]) + Response.new(true, access_token, response) + else + raise OAuthResponseError.new(response) + end + end end def remote_encryption_key(access_token) @@ -144,9 +154,11 @@ def ensure_credentials(try_again = true) access_token: access_token, multiresp: multiresp.responses.present? ? multiresp : nil } - rescue ResponseError => error + rescue ActiveMerchant::OAuthResponseError => e + raise e + rescue ResponseError => e # retry to generate a new access_token when the provided one is expired - raise error unless try_again && %w(401 404).include?(error.response.code) && @options[:access_token].present? + raise e unless retry?(try_again, e, :access_token) @options.delete(:access_token) @options.delete(:encryption_key) @@ -206,9 +218,11 @@ def commit(action, body, options, try_again = true) multiresp.process { resp } multiresp + rescue ActiveMerchant::OAuthResponseError => e + raise OAuthResponseError.new(e) 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? + if retry?(try_again, e, :encryption_key) @options.delete(:encryption_key) commit(action, body, options, false) else @@ -217,6 +231,10 @@ def commit(action, body, options, try_again = true) end end + def retry?(try_again, error, key) + try_again && %w(401 404).include?(error.response.code) && @options[key].present? + end + def success_from(action, response) case action when 'capture/transaction/refund' diff --git a/lib/active_merchant/billing/gateways/authorize_net.rb b/lib/active_merchant/billing/gateways/authorize_net.rb index 6ce77e44789..0fb03993cfd 100644 --- a/lib/active_merchant/billing/gateways/authorize_net.rb +++ b/lib/active_merchant/billing/gateways/authorize_net.rb @@ -85,11 +85,10 @@ class AuthorizeNetGateway < Gateway AVS_REASON_CODES = %w(27 45) TRACKS = { - 1 => /^%(?.)(?[\d]{1,19}+)\^(?.{2,26})\^(?[\d]{0,4}|\^)(?[\d]{0,3}|\^)(?.*)\?\Z/, - 2 => /\A;(?[\d]{1,19}+)=(?[\d]{0,4}|=)(?[\d]{0,3}|=)(?.*)\?\Z/ + 1 => /^%(?.)(?\d{1,19}+)\^(?.{2,26})\^(?\d{0,4}|\^)(?\d{0,3}|\^)(?.*)\?\Z/, + 2 => /\A;(?\d{1,19}+)=(?\d{0,4}|=)(?\d{0,3}|=)(?.*)\?\Z/ }.freeze - APPLE_PAY_DATA_DESCRIPTOR = 'COMMON.APPLE.INAPP.PAYMENT' PAYMENT_METHOD_NOT_SUPPORTED_ERROR = '155' INELIGIBLE_FOR_ISSUING_CREDIT_ERROR = '54' @@ -165,7 +164,7 @@ def credit(amount, payment, options = {}) xml.transactionType('refundTransaction') xml.amount(amount(amount)) - add_payment_source(xml, payment, options, :credit) + add_payment_method(xml, payment, options, :credit) xml.refTransId(transaction_id_from(options[:transaction_id])) if options[:transaction_id] add_invoice(xml, 'refundTransaction', options) add_customer_data(xml, payment, options) @@ -262,7 +261,7 @@ def add_auth_purchase(xml, transaction_type, amount, payment, options) xml.transactionRequest do xml.transactionType(transaction_type) xml.amount(amount(amount)) - add_payment_source(xml, payment, options) + add_payment_method(xml, payment, options) add_invoice(xml, transaction_type, options) add_tax_fields(xml, options) add_duty_fields(xml, options) @@ -273,6 +272,7 @@ def add_auth_purchase(xml, transaction_type, amount, payment, options) add_market_type_device_type(xml, payment, options) add_settings(xml, payment, options) add_user_fields(xml, amount, options) + add_surcharge_fields(xml, options) add_ship_from_address(xml, options) add_processing_options(xml, options) add_subsequent_auth_information(xml, options) @@ -287,8 +287,9 @@ def add_cim_auth_purchase(xml, transaction_type, amount, payment, options) add_tax_fields(xml, options) add_shipping_fields(xml, options) add_duty_fields(xml, options) - add_payment_source(xml, payment, options) + add_payment_method(xml, payment, options) add_invoice(xml, transaction_type, options) + add_surcharge_fields(xml, options) add_tax_exempt_status(xml, options) end end @@ -362,7 +363,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 @@ -407,20 +408,27 @@ def normal_void(authorization, options) end end - def add_payment_source(xml, source, options, action = nil) - return unless source + def add_payment_method(xml, payment_method, options, action = nil) + return unless payment_method - if source.is_a?(String) - add_token_payment_method(xml, source, options) - elsif card_brand(source) == 'check' - add_check(xml, source) - elsif card_brand(source) == 'apple_pay' - add_apple_pay_payment_token(xml, source) + case payment_method + when String + add_token_payment_method(xml, payment_method, options) + when Check + add_check(xml, payment_method) else - add_credit_card(xml, source, action) + if network_token?(payment_method, options, action) + add_network_token(xml, payment_method) + else + add_credit_card(xml, payment_method, action) + end end end + def network_token?(payment_method, options, action) + payment_method.instance_of?(NetworkTokenizationCreditCard) && action != :credit + end + def camel_case_lower(key) String(key).split('_').inject([]) { |buffer, e| buffer.push(buffer.empty? ? e : e.capitalize) }.join end @@ -499,7 +507,6 @@ def add_credit_card(xml, credit_card, action) xml.cardNumber(truncate(credit_card.number, 16)) xml.expirationDate(format(credit_card.month, :two_digits) + '/' + format(credit_card.year, :four_digits)) xml.cardCode(credit_card.verification_value) if credit_card.valid_card_verification_value?(credit_card.verification_value, credit_card.brand) - xml.cryptogram(credit_card.payment_cryptogram) if credit_card.is_a?(NetworkTokenizationCreditCard) && action != :credit end end end @@ -526,17 +533,20 @@ def add_token_payment_method(xml, token, options) xml.customerPaymentProfileId(customer_payment_profile_id) end - def add_apple_pay_payment_token(xml, apple_pay_payment_token) + def add_network_token(xml, payment_method) xml.payment do - xml.opaqueData do - xml.dataDescriptor(APPLE_PAY_DATA_DESCRIPTOR) - xml.dataValue(Base64.strict_encode64(apple_pay_payment_token.payment_data.to_json)) + xml.creditCard do + xml.cardNumber(truncate(payment_method.number, 16)) + xml.expirationDate(format(payment_method.month, :two_digits) + '/' + format(payment_method.year, :four_digits)) + xml.isPaymentToken(true) + xml.cryptogram(payment_method.payment_cryptogram) end end end def add_market_type_device_type(xml, payment, options) - return if payment.is_a?(String) || card_brand(payment) == 'check' || card_brand(payment) == 'apple_pay' + return unless payment.is_a?(CreditCard) + return if payment.is_a?(NetworkTokenizationCreditCard) if valid_track_data xml.retail do @@ -604,6 +614,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) @@ -613,7 +624,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 @@ -700,6 +711,16 @@ def add_duty_fields(xml, options) end end + def add_surcharge_fields(xml, options) + surcharge = options[:surcharge] if options[:surcharge] + if surcharge.is_a?(Hash) + xml.surcharge do + xml.amount(amount(surcharge[:amount].to_i)) if surcharge[:amount] + xml.description(surcharge[:description]) if surcharge[:description] + end + end + end + def add_shipping_fields(xml, options) shipping = options[:shipping] if shipping.is_a?(Hash) @@ -753,13 +774,7 @@ def create_customer_payment_profile(credit_card, options) xml.customerProfileId options[:customer_profile_id] xml.paymentProfile do add_billing_address(xml, credit_card, options) - xml.payment do - xml.creditCard do - xml.cardNumber(truncate(credit_card.number, 16)) - xml.expirationDate(format(credit_card.year, :four_digits) + '-' + format(credit_card.month, :two_digits)) - xml.cardCode(credit_card.verification_value) if credit_card.verification_value - end - end + add_credit_card(xml, credit_card, :cim_store_update) end end end @@ -775,13 +790,7 @@ def create_customer_profile(credit_card, options) xml.customerType('individual') add_billing_address(xml, credit_card, options) add_shipping_address(xml, options, 'shipToList') - xml.payment do - xml.creditCard do - xml.cardNumber(truncate(credit_card.number, 16)) - xml.expirationDate(format(credit_card.year, :four_digits) + '-' + format(credit_card.month, :two_digits)) - xml.cardCode(credit_card.verification_value) if credit_card.verification_value - end - end + add_credit_card(xml, credit_card, :cim_store) end 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..d6dde0dea6f 100644 --- a/lib/active_merchant/billing/gateways/authorize_net_arb.rb +++ b/lib/active_merchant/billing/gateways/authorize_net_arb.rb @@ -208,9 +208,9 @@ def add_subscription(xml, options) # The amount to be billed to the customer # for each payment in the subscription xml.tag!('amount', amount(options[:amount])) if options[:amount] - if trial = options[:trial] + if trial = options[:trial] && (trial[:amount]) # The amount to be charged for each payment during a trial period (conditional) - xml.tag!('trialAmount', amount(trial[:amount])) if trial[:amount] + xml.tag!('trialAmount', amount(trial[:amount])) end # Contains either the customer’s credit card # or bank account payment information @@ -260,9 +260,9 @@ def add_payment_schedule(xml, options) # Contains information about the interval of time between payments add_interval(xml, options) add_duration(xml, options) - if trial = options[:trial] + if trial = options[:trial] && (trial[:occurrences]) # Number of billing occurrences or payments in the trial period (optional) - xml.tag!('trialOccurrences', trial[:occurrences]) if trial[:occurrences] + xml.tag!('trialOccurrences', trial[:occurrences]) end end end @@ -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/authorize_net_cim.rb b/lib/active_merchant/billing/gateways/authorize_net_cim.rb index 09eff729308..0fb0c866cda 100644 --- a/lib/active_merchant/billing/gateways/authorize_net_cim.rb +++ b/lib/active_merchant/billing/gateways/authorize_net_cim.rb @@ -695,9 +695,7 @@ def add_transaction(xml, transaction) add_order(xml, transaction[:order]) if transaction[:order].present? end - if %i[auth_capture auth_only capture_only].include?(transaction[:type]) - xml.tag!('recurringBilling', transaction[:recurring_billing]) if transaction.has_key?(:recurring_billing) - end + xml.tag!('recurringBilling', transaction[:recurring_billing]) if %i[auth_capture auth_only capture_only].include?(transaction[:type]) && transaction.has_key?(:recurring_billing) tag_unless_blank(xml, 'cardCode', transaction[:card_code]) unless %i[void refund prior_auth_capture].include?(transaction[:type]) end end 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/beanstream_core.rb b/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb index b794899c579..87e5c89ba5e 100644 --- a/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb +++ b/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb @@ -228,7 +228,7 @@ def add_address(post, options) if billing_address = options[:billing_address] || options[:address] post[:ordName] = billing_address[:name] - post[:ordPhoneNumber] = billing_address[:phone] + post[:ordPhoneNumber] = billing_address[:phone] || billing_address[:phone_number] post[:ordAddress1] = billing_address[:address1] post[:ordAddress2] = billing_address[:address2] post[:ordCity] = billing_address[:city] @@ -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..4f2f7eac987 100644 --- a/lib/active_merchant/billing/gateways/blue_pay.rb +++ b/lib/active_merchant/billing/gateways/blue_pay.rb @@ -344,14 +344,18 @@ 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) # The bp20api has max one value per form field. - response_fields = Hash[CGI::parse(body).map { |k, v| [k.upcase, v.first] }] + response_fields = CGI::parse(body).map { |k, v| [k.upcase, v.first] }.to_h return parse_recurring(response_fields) if response_fields.include? 'REBILL_ID' @@ -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 de2ec414d5c..8490f0643d9 100644 --- a/lib/active_merchant/billing/gateways/blue_snap.rb +++ b/lib/active_merchant/billing/gateways/blue_snap.rb @@ -260,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) @@ -366,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 @@ -444,10 +446,10 @@ def parse_metadata_entry(node) end def parse_element(parsed, node) - if !node.elements.empty? - node.elements.each { |e| parse_element(parsed, e) } - else + if node.elements.empty? parsed[node.name.downcase] = node.text + else + node.elements.each { |e| parse_element(parsed, e) } end end @@ -457,8 +459,8 @@ def api_request(action, request, verb, payment_method_details, options) e.response end - def commit(action, options, verb = :post, payment_method_details = PaymentMethodDetails.new()) - request = build_xml_request(action, payment_method_details) { |doc| yield(doc) } + def commit(action, options, verb = :post, payment_method_details = PaymentMethodDetails.new(), &block) + request = build_xml_request(action, payment_method_details, &block) response = api_request(action, request, verb, payment_method_details, options) parsed = parse(response) 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 8d4883dd4f7..778c6bc64eb 100644 --- a/lib/active_merchant/billing/gateways/borgun.rb +++ b/lib/active_merchant/billing/gateways/borgun.rb @@ -96,6 +96,7 @@ 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] @@ -105,13 +106,18 @@ def add_3ds_fields(post, options) def add_3ds_preauth_fields(post, options) post[:SaleDescription] = options[:sale_description] || '' - post[:MerchantReturnURL] = options[:merchant_return_url] if options[:merchant_return_url] + 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)] - post[:TrCurrencyExponent] = options[:currency_exponent] || 0 if options[:apply_3d_secure] == '1' + # 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 @@ -166,7 +172,7 @@ def commit(action, post, options = {}) success, message_from(success, pairs), pairs, - authorization: authorization_from(pairs), + authorization: authorization_from(pairs, options), test: test? ) end @@ -179,12 +185,12 @@ 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], @@ -194,6 +200,8 @@ def authorization_from(response) response[:tramount], response[:trcurrency] ].join('|') + + authorization == '|||||||' ? nil : authorization end def split_authorization(authorization) diff --git a/lib/active_merchant/billing/gateways/braintree/token_nonce.rb b/lib/active_merchant/billing/gateways/braintree/token_nonce.rb index 37417dd732a..dc9a3e0bc90 100644 --- a/lib/active_merchant/billing/gateways/braintree/token_nonce.rb +++ b/lib/active_merchant/billing/gateways/braintree/token_nonce.rb @@ -29,7 +29,7 @@ def create_token_nonce_for_payment_method(payment_method) json_response = JSON.parse(resp) message = json_response['errors'].map { |err| err['message'] }.join("\n") if json_response['errors'].present? - token = json_response.dig('data', 'tokenizeUsBankAccount', 'paymentMethod', 'id') + token = token_from(payment_method, json_response) return token, message end @@ -41,7 +41,7 @@ def client_token private - def graphql_query + def graphql_bank_query <<-GRAPHQL mutation TokenizeUsBankAccount($input: TokenizeUsBankAccountInput!) { tokenizeUsBankAccount(input: $input) { @@ -58,6 +58,23 @@ def graphql_query GRAPHQL end + def graphql_credit_query + <<-GRAPHQL + mutation TokenizeCreditCard($input: TokenizeCreditCardInput!) { + tokenizeCreditCard(input: $input) { + paymentMethod { + id + details { + ... on CreditCardDetails { + last4 + } + } + } + } + } + GRAPHQL + end + def billing_address_from_options return nil if options[:billing_address].blank? @@ -72,7 +89,42 @@ def billing_address_from_options }.compact end + def build_nonce_credit_card_request(payment_method) + billing_address = billing_address_from_options + key_replacements = { city: :locality, state: :region, zipCode: :postalCode } + billing_address&.transform_keys! { |key| key_replacements[key] || key } + { + creditCard: { + number: payment_method.number, + expirationYear: payment_method.year.to_s, + expirationMonth: payment_method.month.to_s.rjust(2, '0'), + cvv: payment_method.verification_value, + cardholderName: payment_method.name, + billingAddress: billing_address + } + } + end + def build_nonce_request(payment_method) + input = payment_method.is_a?(Check) ? build_nonce_bank_request(payment_method) : build_nonce_credit_card_request(payment_method) + graphql_query = payment_method.is_a?(Check) ? graphql_bank_query : graphql_credit_query + + { + clientSdkMetadata: { + platform: 'web', + source: 'client', + integration: 'custom', + sessionId: SecureRandom.uuid, + version: '3.83.0' + }, + query: graphql_query, + variables: { + input: input + } + }.to_json + end + + def build_nonce_bank_request(payment_method) input = { usBankAccount: { achMandate: options[:ach_mandate], @@ -94,19 +146,12 @@ def build_nonce_request(payment_method) } end - { - clientSdkMetadata: { - platform: 'web', - source: 'client', - integration: 'custom', - sessionId: SecureRandom.uuid, - version: '3.83.0' - }, - query: graphql_query, - variables: { - input: input - } - }.to_json + input + end + + def token_from(payment_method, response) + tokenized_field = payment_method.is_a?(Check) ? 'tokenizeUsBankAccount' : 'tokenizeCreditCard' + response.dig('data', tokenized_field, 'paymentMethod', 'id') end end end diff --git a/lib/active_merchant/billing/gateways/braintree_blue.rb b/lib/active_merchant/billing/gateways/braintree_blue.rb index 146470d66ba..8a9782e3baf 100644 --- a/lib/active_merchant/billing/gateways/braintree_blue.rb +++ b/lib/active_merchant/billing/gateways/braintree_blue.rb @@ -75,9 +75,12 @@ def initialize(options = {}) @braintree_gateway = Braintree::Gateway.new(@configuration) end - def setup_purchase + def setup_purchase(options = {}) + post = {} + add_merchant_account_id(post, options) + commit do - Response.new(true, 'Client token created', { client_token: @braintree_gateway.client_token.generate }) + Response.new(true, 'Client token created', { client_token: @braintree_gateway.client_token.generate(post) }) end end @@ -106,6 +109,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 @@ -149,6 +154,10 @@ def verify(creditcard, options = {}) } } } + if merchant_account_id = (options[:merchant_account_id] || @merchant_account_id) + payload[:options] = { merchant_account_id: merchant_account_id } + end + commit do result = @braintree_gateway.verification.create(payload) response = Response.new(result.success?, message_from_transaction_result(result), response_options(result)) @@ -193,16 +202,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 @@ -267,19 +280,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 @@ -348,6 +363,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], @@ -369,8 +388,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) @@ -420,6 +439,8 @@ def response_options(result) end def avs_code_from(transaction) + return unless transaction + transaction.avs_error_response_code || avs_mapping["street: #{transaction.avs_street_address_response_code}, zip: #{transaction.avs_postal_code_response_code}"] end @@ -484,6 +505,55 @@ def additional_processor_response_from_result(result) result.transaction&.additional_processor_response end + def payment_instrument_type(result) + result&.payment_instrument_type + end + + def credit_card_details(result) + if result + { + 'masked_number' => result.credit_card_details&.masked_number, + 'bin' => result.credit_card_details&.bin, + 'last_4' => result.credit_card_details&.last_4, + 'card_type' => result.credit_card_details&.card_type, + 'token' => result.credit_card_details&.token, + 'debit' => result.credit_card_details&.debit, + 'prepaid' => result.credit_card_details&.prepaid, + 'issuing_bank' => result.credit_card_details&.issuing_bank, + 'country_of_issuance' => result.credit_card_details&.country_of_issuance + } + end + end + + def network_token_details(result) + if result + { + 'debit' => result.network_token_details&.debit, + 'prepaid' => result.network_token_details&.prepaid, + 'issuing_bank' => result.network_token_details&.issuing_bank + } + end + end + + def google_pay_details(result) + if result + { + 'debit' => result.google_pay_details&.debit, + 'prepaid' => result.google_pay_details&.prepaid + } + end + end + + def apple_pay_details(result) + if result + { + 'debit' => result.apple_pay_details&.debit, + 'prepaid' => result.apple_pay_details&.prepaid, + 'issuing_bank' => result.apple_pay_details&.issuing_bank + } + end + 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 @@ -543,7 +613,15 @@ def customer_hash(customer, include_credit_cards = false) def transaction_hash(result) unless result.success? return { 'processor_response_code' => response_code_from_result(result), - 'additional_processor_response' => additional_processor_response_from_result(result) } + 'additional_processor_response' => additional_processor_response_from_result(result), + 'payment_instrument_type' => payment_instrument_type(result.transaction), + 'credit_card_details' => credit_card_details(result.transaction), + 'network_token_details' => network_token_details(result.transaction), + 'google_pay_details' => google_pay_details(result.transaction), + 'apple_pay_details' => apple_pay_details(result.transaction), + 'avs_response_code' => avs_code_from(result.transaction), + 'cvv_response_code' => result.transaction&.cvv_response_code, + 'gateway_message' => result.message } end transaction = result.transaction @@ -559,6 +637,14 @@ def transaction_hash(result) vault_customer = nil end + credit_card_details = credit_card_details(transaction) + + network_token_details = network_token_details(transaction) + + google_pay_details = google_pay_details(transaction) + + apple_pay_details = apple_pay_details(transaction) + customer_details = { 'id' => transaction.customer_details.id, 'email' => transaction.customer_details.email, @@ -584,12 +670,10 @@ def transaction_hash(result) 'postal_code' => transaction.shipping_details.postal_code, 'country_name' => transaction.shipping_details.country_name } - credit_card_details = { - 'masked_number' => transaction.credit_card_details.masked_number, - '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 + + paypal_details = { + 'payer_id' => transaction.paypal_details.payer_id, + 'payer_email' => transaction.paypal_details.payer_email } if transaction.risk_data @@ -603,20 +687,35 @@ 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, + 'network_token_details' => network_token_details, + 'apple_pay_details' => apple_pay_details, + 'google_pay_details' => google_pay_details, + 'paypal_details' => paypal_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, + 'payment_instrument_type' => payment_instrument_type(transaction) } end @@ -627,8 +726,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, @@ -652,6 +750,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) @@ -662,6 +761,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] @@ -728,6 +829,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] @@ -791,15 +901,41 @@ def xid_or_ds_trans_id(three_d_secure_opts) end def add_stored_credential_data(parameters, credit_card_or_vault_id, options) + # Braintree has informed us that the stored_credential mapping may be incorrect + # In order to prevent possible breaking changes we will only apply the new logic if + # specifically requested. This will be the default behavior in a future release. return unless (stored_credential = options[:stored_credential]) - parameters[:external_vault] = {} - if stored_credential[:initial_transaction] - parameters[:external_vault][:status] = 'will_vault' + add_external_vault(parameters, options) + + if options[:stored_credentials_v2] + stored_credentials_v2(parameters, stored_credential) else - parameters[:external_vault][:status] = 'vaulted' - parameters[:external_vault][:previous_network_transaction_id] = stored_credential[:network_transaction_id] + stored_credentials_v1(parameters, stored_credential) end + end + + def stored_credentials_v2(parameters, stored_credential) + # Differences between v1 and v2 are + # initial_transaction + recurring/installment should be labeled {{reason_type}}_first + # unscheduled in AM should map to '' at BT because unscheduled here means not on a fixed timeline or fixed amount + case stored_credential[:reason_type] + when 'recurring', 'installment' + if stored_credential[:initial_transaction] + parameters[:transaction_source] = "#{stored_credential[:reason_type]}_first" + else + parameters[:transaction_source] = stored_credential[:reason_type] + end + when 'recurring_first', 'moto' + parameters[:transaction_source] = stored_credential[:reason_type] + when 'unscheduled' + parameters[:transaction_source] = stored_credential[:initiator] == 'merchant' ? stored_credential[:reason_type] : '' + else + parameters[:transaction_source] = '' + end + end + + def stored_credentials_v1(parameters, stored_credential) if stored_credential[:initiator] == 'merchant' if stored_credential[:reason_type] == 'installment' parameters[:transaction_source] = 'recurring' @@ -813,56 +949,99 @@ def add_stored_credential_data(parameters, credit_card_or_vault_id, options) end end + def add_external_vault(parameters, options = {}) + stored_credential = options[:stored_credential] + parameters[:external_vault] = {} + if stored_credential[:initial_transaction] + parameters[:external_vault][:status] = 'will_vault' + else + parameters[:external_vault][:status] = 'vaulted' + parameters[:external_vault][:previous_network_transaction_id] = options[:network_transaction_id] || stored_credential[:network_transaction_id] + end + end + 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 @@ -889,13 +1068,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) @@ -921,7 +1103,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 @@ -932,7 +1114,7 @@ def create_customer_from_bank_account(payment_method, options) Response.new( result.success?, message_from_result(result), - { customer_vault_id: customer_id, 'exists': true } + { customer_vault_id: customer_id, exists: true } ) end end diff --git a/lib/active_merchant/billing/gateways/braintree_orange.rb b/lib/active_merchant/billing/gateways/braintree_orange.rb index f56502eb7a0..a4f85d879a7 100644 --- a/lib/active_merchant/billing/gateways/braintree_orange.rb +++ b/lib/active_merchant/billing/gateways/braintree_orange.rb @@ -1,4 +1,4 @@ -require 'active_merchant/billing/gateways/smart_ps.rb' +require 'active_merchant/billing/gateways/smart_ps' require 'active_merchant/billing/gateways/braintree/braintree_common' module ActiveMerchant #:nodoc: diff --git a/lib/active_merchant/billing/gateways/card_connect.rb b/lib/active_merchant/billing/gateways/card_connect.rb index 8895e72bad3..6a803aeb322 100644 --- a/lib/active_merchant/billing/gateways/card_connect.rb +++ b/lib/active_merchant/billing/gateways/card_connect.rb @@ -146,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? diff --git a/lib/active_merchant/billing/gateways/card_stream.rb b/lib/active_merchant/billing/gateways/card_stream.rb index 76c8f318519..a20799c198d 100644 --- a/lib/active_merchant/billing/gateways/card_stream.rb +++ b/lib/active_merchant/billing/gateways/card_stream.rb @@ -248,12 +248,10 @@ def add_invoice(post, credit_card_or_reference, money, options) add_pair(post, :orderRef, options[:description] || options[:order_id], required: true) add_pair(post, :statementNarrative1, options[:merchant_name]) if options[:merchant_name] add_pair(post, :statementNarrative2, options[:dynamic_descriptor]) if options[:dynamic_descriptor] - if credit_card_or_reference.respond_to?(:number) - if %w[american_express diners_club].include?(card_brand(credit_card_or_reference).to_s) - add_pair(post, :item1Quantity, 1) - add_pair(post, :item1Description, (options[:description] || options[:order_id]).slice(0, 15)) - add_pair(post, :item1GrossValue, localized_amount(money, options[:currency] || currency(money))) - end + if credit_card_or_reference.respond_to?(:number) && %w[american_express diners_club].include?(card_brand(credit_card_or_reference).to_s) + add_pair(post, :item1Quantity, 1) + add_pair(post, :item1Description, (options[:description] || options[:order_id]).slice(0, 15)) + add_pair(post, :item1GrossValue, localized_amount(money, options[:currency] || currency(money))) end add_pair(post, :type, options[:type] || '1') diff --git a/lib/active_merchant/billing/gateways/cashnet.rb b/lib/active_merchant/billing/gateways/cashnet.rb index 340210415c3..cd0c13c6e38 100644 --- a/lib/active_merchant/billing/gateways/cashnet.rb +++ b/lib/active_merchant/billing/gateways/cashnet.rb @@ -150,7 +150,7 @@ def parse(body) match = body.match(/(.*)<\/cngateway>/) return nil unless match - Hash[CGI::parse(match[1]).map { |k, v| [k.to_sym, v.first] }] + CGI::parse(match[1]).map { |k, v| [k.to_sym, v.first] }.to_h end def handle_response(response) diff --git a/lib/active_merchant/billing/gateways/cecabank.rb b/lib/active_merchant/billing/gateways/cecabank.rb index d85b48f7ed9..18a0aed5d93 100644 --- a/lib/active_merchant/billing/gateways/cecabank.rb +++ b/lib/active_merchant/billing/gateways/cecabank.rb @@ -1,248 +1,15 @@ +require 'active_merchant/billing/gateways/cecabank/cecabank_xml' +require 'active_merchant/billing/gateways/cecabank/cecabank_json' + module ActiveMerchant #:nodoc: module Billing #:nodoc: class CecabankGateway < Gateway - self.test_url = 'https://tpv.ceca.es' - self.live_url = 'https://pgw.ceca.es' - - self.supported_countries = ['ES'] - self.supported_cardtypes = %i[visa master american_express] - self.homepage_url = 'http://www.ceca.es/es/' - self.display_name = 'Cecabank' - self.default_currency = 'EUR' - self.money_format = :cents - - #### CECA's MAGIC NUMBERS - CECA_NOTIFICATIONS_URL = 'NONE' - CECA_ENCRIPTION = 'SHA2' - CECA_DECIMALS = '2' - CECA_MODE = 'SSL' - CECA_UI_LESS_LANGUAGE = 'XML' - CECA_UI_LESS_LANGUAGE_REFUND = '1' - CECA_UI_LESS_REFUND_PAGE = 'anulacion_xml' - CECA_ACTION_REFUND = 'anulaciones/anularParcial' # use partial refund's URL to avoid time frame limitations and decision logic on client side - CECA_ACTION_PURCHASE = 'tpv/compra' - CECA_CURRENCIES_DICTIONARY = { 'EUR' => 978, 'USD' => 840, 'GBP' => 826 } - - # Creates a new CecabankGateway - # - # The gateway requires four values for connection to be passed - # in the +options+ hash. - # - # ==== Options - # - # * :merchant_id -- Cecabank's merchant_id (REQUIRED) - # * :acquirer_bin -- Cecabank's acquirer_bin (REQUIRED) - # * :terminal_id -- Cecabank's terminal_id (REQUIRED) - # * :key -- Cecabank's cypher key (REQUIRED) - # * :test -- +true+ or +false+. If true, perform transactions against the test server. - # Otherwise, perform transactions against the production server. - def initialize(options = {}) - requires!(options, :merchant_id, :acquirer_bin, :terminal_id, :key) - super - end - - # Perform a purchase, which is essentially an authorization and capture in a single operation. - # - # ==== Parameters - # - # * money -- The amount to be purchased as an Integer value in cents. - # * creditcard -- The CreditCard details for the transaction. - # * options -- A hash of optional parameters. - # - # ==== Options - # - # * :order_id -- order_id passed used purchase. (REQUIRED) - # * :currency -- currency. Supported: EUR, USD, GBP. - # * :description -- description to be pased to the gateway. - def purchase(money, creditcard, options = {}) - requires!(options, :order_id) - - post = { 'Descripcion' => options[:description], - 'Num_operacion' => options[:order_id], - 'Idioma' => CECA_UI_LESS_LANGUAGE, - 'Pago_soportado' => CECA_MODE, - 'URL_OK' => CECA_NOTIFICATIONS_URL, - 'URL_NOK' => CECA_NOTIFICATIONS_URL, - 'Importe' => amount(money), - 'TipoMoneda' => CECA_CURRENCIES_DICTIONARY[options[:currency] || currency(money)] } - - add_creditcard(post, creditcard) - - commit(CECA_ACTION_PURCHASE, post) - end - - # Refund a transaction. - # - # This transaction indicates to the gateway that - # money should flow from the merchant to the customer. - # - # ==== Parameters - # - # * money -- The amount to be credited to the customer as an Integer value in cents. - # * identification -- The reference given from the gateway on purchase (reference, not operation). - # * options -- A hash of parameters. - def refund(money, identification, options = {}) - reference, order_id = split_authorization(identification) - - post = { 'Referencia' => reference, - 'Num_operacion' => order_id, - 'Idioma' => CECA_UI_LESS_LANGUAGE_REFUND, - 'Pagina' => CECA_UI_LESS_REFUND_PAGE, - 'Importe' => amount(money), - 'TipoMoneda' => CECA_CURRENCIES_DICTIONARY[options[:currency] || currency(money)] } - - commit(CECA_ACTION_REFUND, post) - end - - def supports_scrubbing - true - end - - def scrub(transcript) - transcript. - gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). - gsub(%r((&?pan=)[^&]*)i, '\1[FILTERED]'). - gsub(%r((&?cvv2=)[^&]*)i, '\1[FILTERED]') - end - - private - - def add_creditcard(post, creditcard) - post['PAN'] = creditcard.number - post['Caducidad'] = expdate(creditcard) - post['CVV2'] = creditcard.verification_value - post['Pago_elegido'] = CECA_MODE - end + self.abstract_class = true - def expdate(creditcard) - "#{format(creditcard.year, :four_digits)}#{format(creditcard.month, :two_digits)}" - end - - def parse(body) - response = {} - - root = REXML::Document.new(body).root - - response[:success] = (root.attributes['valor'] == 'OK') - response[:date] = root.attributes['fecha'] - response[:operation_number] = root.attributes['numeroOperacion'] - response[:message] = root.attributes['valor'] - - if root.elements['OPERACION'] - response[:operation_type] = root.elements['OPERACION'].attributes['tipo'] - response[:amount] = root.elements['OPERACION/importe'].text.strip - end - - response[:description] = root.elements['OPERACION/descripcion'].text if root.elements['OPERACION/descripcion'] - response[:authorization_number] = root.elements['OPERACION/numeroAutorizacion'].text if root.elements['OPERACION/numeroAutorizacion'] - response[:reference] = root.elements['OPERACION/referencia'].text if root.elements['OPERACION/referencia'] - response[:pan] = root.elements['OPERACION/pan'].text if root.elements['OPERACION/pan'] - - if root.elements['ERROR'] - response[:error_code] = root.elements['ERROR/codigo'].text - response[:error_message] = root.elements['ERROR/descripcion'].text - else - if root.elements['OPERACION'].attributes['numeroOperacion'] == '000' - response[:authorization] = root.elements['OPERACION/numeroAutorizacion'].text if root.elements['OPERACION/numeroAutorizacion'] - else - response[:authorization] = root.attributes['numeroOperacion'] - end - end - - return response - rescue REXML::ParseException => e - response[:success] = false - response[:message] = 'Unable to parse the response.' - response[:error_message] = e.message - response - end - - def commit(action, parameters) - parameters.merge!( - 'Cifrado' => CECA_ENCRIPTION, - 'Firma' => generate_signature(action, parameters), - 'Exponente' => CECA_DECIMALS, - 'MerchantID' => options[:merchant_id], - 'AcquirerBIN' => options[:acquirer_bin], - 'TerminalID' => options[:terminal_id] - ) - url = (test? ? self.test_url : self.live_url) + "/tpvweb/#{action}.action" - xml = ssl_post("#{url}?", post_data(parameters)) - response = parse(xml) - Response.new( - response[:success], - message_from(response), - response, - test: test?, - authorization: build_authorization(response), - error_code: response[:error_code] - ) - end - - def message_from(response) - if response[:message] == 'ERROR' && response[:error_message] - response[:error_message] - elsif response[:error_message] - "#{response[:message]} #{response[:error_message]}" - else - response[:message] - end - end - - def post_data(params) - return nil unless params - - params.map do |key, value| - next if value.blank? - - if value.is_a?(Hash) - h = {} - value.each do |k, v| - h["#{key}.#{k}"] = v unless v.blank? - end - post_data(h) - else - "#{key}=#{CGI.escape(value.to_s)}" - end - end.compact.join('&') - end - - def build_authorization(response) - [response[:reference], response[:authorization]].join('|') - end - - def split_authorization(authorization) - authorization.split('|') - end + def self.new(options = {}) + return CecabankJsonGateway.new(options) if options[:is_rest_json] - def generate_signature(action, parameters) - signature_fields = - case action - when CECA_ACTION_REFUND - options[:key].to_s + - options[:merchant_id].to_s + - options[:acquirer_bin].to_s + - options[:terminal_id].to_s + - parameters['Num_operacion'].to_s + - parameters['Importe'].to_s + - parameters['TipoMoneda'].to_s + - CECA_DECIMALS + - parameters['Referencia'].to_s + - CECA_ENCRIPTION - else - options[:key].to_s + - options[:merchant_id].to_s + - options[:acquirer_bin].to_s + - options[:terminal_id].to_s + - parameters['Num_operacion'].to_s + - parameters['Importe'].to_s + - parameters['TipoMoneda'].to_s + - CECA_DECIMALS + - CECA_ENCRIPTION + - CECA_NOTIFICATIONS_URL + - CECA_NOTIFICATIONS_URL - end - Digest::SHA2.hexdigest(signature_fields) + CecabankXmlGateway.new(options) end end end diff --git a/lib/active_merchant/billing/gateways/cecabank/cecabank_common.rb b/lib/active_merchant/billing/gateways/cecabank/cecabank_common.rb new file mode 100644 index 00000000000..a397c2955c8 --- /dev/null +++ b/lib/active_merchant/billing/gateways/cecabank/cecabank_common.rb @@ -0,0 +1,36 @@ +module CecabankCommon + #### CECA's MAGIC NUMBERS + CECA_ENCRIPTION = 'SHA2' + CECA_CURRENCIES_DICTIONARY = { 'EUR' => 978, 'USD' => 840, 'GBP' => 826 } + + def self.included(base) + base.supported_countries = ['ES'] + base.supported_cardtypes = %i[visa master american_express] + base.homepage_url = 'http://www.ceca.es/es/' + base.display_name = 'Cecabank' + base.default_currency = 'EUR' + base.money_format = :cents + end + + # Creates a new CecabankGateway + # + # The gateway requires four values for connection to be passed + # in the +options+ hash. + # + # ==== Options + # + # * :merchant_id -- Cecabank's merchant_id (REQUIRED) + # * :acquirer_bin -- Cecabank's acquirer_bin (REQUIRED) + # * :terminal_id -- Cecabank's terminal_id (REQUIRED) + # * :cypher_key -- Cecabank's cypher key (REQUIRED) + # * :test -- +true+ or +false+. If true, perform transactions against the test server. + # Otherwise, perform transactions against the production server. + def initialize(options = {}) + requires!(options, :merchant_id, :acquirer_bin, :terminal_id, :cypher_key) + super + end + + def supports_scrubbing? + true + end +end diff --git a/lib/active_merchant/billing/gateways/cecabank/cecabank_json.rb b/lib/active_merchant/billing/gateways/cecabank/cecabank_json.rb new file mode 100644 index 00000000000..e24df79c05b --- /dev/null +++ b/lib/active_merchant/billing/gateways/cecabank/cecabank_json.rb @@ -0,0 +1,316 @@ +require 'active_merchant/billing/gateways/cecabank/cecabank_common' + +module ActiveMerchant + module Billing + class CecabankJsonGateway < Gateway + include CecabankCommon + + CECA_ACTIONS_DICTIONARY = { + purchase: :REST_AUTORIZACION, + authorize: :REST_PREAUTORIZACION, + capture: :REST_COBRO_PREAUTORIZACION, + refund: :REST_DEVOLUCION, + void: :REST_ANULACION + }.freeze + + CECA_REASON_TYPES = { + installment: :I, + recurring: :R, + unscheduled: :C + }.freeze + + CECA_INITIATOR = { + merchant: :N, + cardholder: :S + }.freeze + + CECA_SCA_TYPES = { + low_value_exemption: :LOW, + transaction_risk_analysis_exemption: :TRA + }.freeze + + self.test_url = 'https://tpv.ceca.es/tpvweb/rest/procesos/' + self.live_url = 'https://pgw.ceca.es/tpvweb/rest/procesos/' + + def authorize(money, creditcard, options = {}) + handle_purchase(:authorize, money, creditcard, options) + end + + def capture(money, identification, options = {}) + authorization, operation_number, _money = identification.split('#') + + post = {} + options[:operation_number] = operation_number + add_auth_invoice_data(:capture, post, money, authorization, options) + + commit('compra', post) + end + + def purchase(money, creditcard, options = {}) + handle_purchase(:purchase, money, creditcard, options) + end + + def void(identification, options = {}) + authorization, operation_number, money = identification.split('#') + options[:operation_number] = operation_number + handle_cancellation(:void, money.to_i, authorization, options) + end + + def refund(money, identification, options = {}) + authorization, operation_number, _money = identification.split('#') + options[:operation_number] = operation_number + handle_cancellation(:refund, money, authorization, options) + end + + def scrub(transcript) + return '' if transcript.blank? + + before_message = transcript.gsub(%r(\\\")i, "'").scan(/{[^>]*}/).first.gsub("'", '"') + request_data = JSON.parse(before_message) + + if @options[:encryption_key] + params = parse(request_data['parametros']) + sensitive_fields = decrypt_sensitive_fields(params['encryptedData']) + filtered_params = filter_params(sensitive_fields) + params['encryptedData'] = encrypt_sensitive_fields(filtered_params) + else + params = filter_params(decode_params(request_data['parametros'])) + end + + request_data['parametros'] = encode_params(params) + before_message = before_message.gsub(%r(\")i, '\\\"') + after_message = request_data.to_json.gsub(%r(\")i, '\\\"') + transcript.sub(before_message, after_message) + end + + private + + def filter_params(params) + params. + gsub(%r(("pan\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("caducidad\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cvv2\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("csc\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("authentication_value\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]') + end + + def decrypt_sensitive_fields(data) + cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt + cipher.key = [@options[:encryption_key]].pack('H*') + cipher.iv = @options[:initiator_vector]&.split('')&.map(&:to_i)&.pack('c*') + cipher.update([data].pack('H*')) + cipher.final + end + + def encrypt_sensitive_fields(data) + cipher = OpenSSL::Cipher.new('AES-256-CBC').encrypt + cipher.key = [@options[:encryption_key]].pack('H*') + cipher.iv = @options[:initiator_vector]&.split('')&.map(&:to_i)&.pack('c*') + encrypted = cipher.update(data.to_json) + cipher.final + encrypted.unpack1('H*') + end + + def handle_purchase(action, money, creditcard, options) + post = { parametros: { accion: CECA_ACTIONS_DICTIONARY[action] } } + + add_invoice(post, money, options) + add_creditcard(post, creditcard) + add_stored_credentials(post, creditcard, options) + add_three_d_secure(post, options) + + commit('compra', post) + end + + def handle_cancellation(action, money, authorization, options = {}) + post = {} + add_auth_invoice_data(action, post, money, authorization, options) + + commit('anulacion', post) + end + + def add_auth_invoice_data(action, post, money, authorization, options) + params = post[:parametros] ||= {} + params[:accion] = CECA_ACTIONS_DICTIONARY[action] + params[:referencia] = authorization + + add_invoice(post, money, options) + end + + def add_encryption(post) + post[:cifrado] = CECA_ENCRIPTION + post[:parametros][:encryptedData] = encrypt_sensitive_fields(post[:parametros][:encryptedData]) if @options[:encryption_key] + end + + def add_signature(post, params_encoded, options) + post[:firma] = Digest::SHA2.hexdigest(@options[:cypher_key].to_s + params_encoded) + end + + def add_merchant_data(post) + params = post[:parametros] ||= {} + + params[:merchantID] = @options[:merchant_id] + params[:acquirerBIN] = @options[:acquirer_bin] + params[:terminalID] = @options[:terminal_id] + end + + def add_invoice(post, money, options) + post[:parametros][:numOperacion] = options[:operation_number] || options[:order_id] + post[:parametros][:importe] = amount(money) + post[:parametros][:tipoMoneda] = CECA_CURRENCIES_DICTIONARY[options[:currency] || currency(money)].to_s + post[:parametros][:exponente] = 2.to_s + end + + def add_creditcard(post, creditcard) + params = post[:parametros] ||= {} + + payment_method = { + pan: creditcard.number, + caducidad: strftime_yyyymm(creditcard) + } + if CreditCard.brand?(creditcard.number) == 'american_express' + payment_method[:csc] = creditcard.verification_value + else + payment_method[:cvv2] = creditcard.verification_value + end + + @options[:encryption_key] ? params[:encryptedData] = payment_method : params.merge!(payment_method) + end + + def add_stored_credentials(post, creditcard, options) + return unless stored_credential = options[:stored_credential] + + return if options[:exemption_type].blank? && !(stored_credential[:reason_type] && stored_credential[:initiator]) + + params = post[:parametros] ||= {} + params[:exencionSCA] = 'MIT' + + requires!(stored_credential, :reason_type, :initiator) + reason_type = CECA_REASON_TYPES[stored_credential[:reason_type].to_sym] + initiator = CECA_INITIATOR[stored_credential[:initiator].to_sym] + params[:tipoCOF] = reason_type + params[:inicioRec] = initiator + if initiator == :S + requires!(options, :recurring_frequency) + params[:finRec] = options[:recurring_end_date] || strftime_yyyymm(creditcard) + params[:frecRec] = options[:recurring_frequency] + end + + network_transaction_id = options[:network_transaction_id].present? ? options[:network_transaction_id] : stored_credential[:network_transaction_id] + params[:mmppTxId] = network_transaction_id unless network_transaction_id.blank? + end + + def add_three_d_secure(post, options) + params = post[:parametros] ||= {} + return unless three_d_secure = options[:three_d_secure] + + params[:exencionSCA] ||= CECA_SCA_TYPES.fetch(options[:exemption_type]&.to_sym, :NONE) + + three_d_response = { + exemption_type: options[:exemption_type], + three_ds_version: three_d_secure[:version], + directory_server_transaction_id: three_d_secure[:ds_transaction_id], + acs_transaction_id: three_d_secure[:acs_transaction_id], + authentication_response_status: three_d_secure[:authentication_response_status], + three_ds_server_trans_id: three_d_secure[:three_ds_server_trans_id], + ecommerce_indicator: three_d_secure[:eci], + enrolled: three_d_secure[:enrolled] + } + + if @options[:encryption_key] + params[:encryptedData].merge!({ authentication_value: three_d_secure[:cavv] }) + else + three_d_response[:authentication_value] = three_d_secure[:cavv] + end + + three_d_response[:amount] = post[:parametros][:importe] + params[:ThreeDsResponse] = three_d_response.to_json + end + + def commit(action, post) + auth_options = { + operation_number: post.dig(:parametros, :numOperacion), + amount: post.dig(:parametros, :importe) + } + + add_encryption(post) + add_merchant_data(post) + + params_encoded = encode_post_parameters(post) + add_signature(post, params_encoded, options) + + response = parse(ssl_post(url(action), post.to_json, headers)) + response[:parametros] = parse(response[:parametros]) if response[:parametros] + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response, auth_options), + network_transaction_id: network_transaction_id_from(response), + test: test?, + error_code: error_code_from(response) + ) + end + + def url(action) + (test? ? self.test_url : self.live_url) + action + end + + def host + URI.parse(url('')).host + end + + def headers + { + 'Content-Type' => 'application/json', + 'Host' => host + } + end + + def parse(string) + JSON.parse(string).with_indifferent_access + rescue JSON::ParserError + parse(decode_params(string)) + end + + def encode_post_parameters(post) + post[:parametros] = encode_params(post[:parametros]) + end + + def encode_params(params) + Base64.strict_encode64(params.is_a?(Hash) ? params.to_json : params) + end + + def decode_params(params) + Base64.decode64(params) + end + + def success_from(response) + response[:codResult].blank? + end + + def message_from(response) + return response[:parametros].to_json if success_from(response) + + response[:paramsEntradaError] || response[:idProceso] + end + + def authorization_from(response, auth_options = {}) + return unless response[:parametros] + + [ + response[:parametros][:referencia], + auth_options[:operation_number], + auth_options[:amount] + ].join('#') + end + + def network_transaction_id_from(response) + response.dig(:parametros, :mmppTxId) + end + + def error_code_from(response) + (response[:codResult] || :paramsEntradaError) unless success_from(response) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/cecabank/cecabank_xml.rb b/lib/active_merchant/billing/gateways/cecabank/cecabank_xml.rb new file mode 100644 index 00000000000..d670e23ab49 --- /dev/null +++ b/lib/active_merchant/billing/gateways/cecabank/cecabank_xml.rb @@ -0,0 +1,220 @@ +require 'active_merchant/billing/gateways/cecabank/cecabank_common' + +module ActiveMerchant + module Billing + class CecabankXmlGateway < Gateway + include CecabankCommon + + self.test_url = 'https://tpv.ceca.es' + self.live_url = 'https://pgw.ceca.es' + + #### CECA's MAGIC NUMBERS + CECA_NOTIFICATIONS_URL = 'NONE' + CECA_DECIMALS = '2' + CECA_MODE = 'SSL' + CECA_UI_LESS_LANGUAGE = 'XML' + CECA_UI_LESS_LANGUAGE_REFUND = '1' + CECA_UI_LESS_REFUND_PAGE = 'anulacion_xml' + CECA_ACTION_REFUND = 'anulaciones/anularParcial' # use partial refund's URL to avoid time frame limitations and decision logic on client side + CECA_ACTION_PURCHASE = 'tpv/compra' + + # Perform a purchase, which is essentially an authorization and capture in a single operation. + # + # ==== Parameters + # + # * money -- The amount to be purchased as an Integer value in cents. + # * creditcard -- The CreditCard details for the transaction. + # * options -- A hash of optional parameters. + # + # ==== Options + # + # * :order_id -- order_id passed used purchase. (REQUIRED) + # * :currency -- currency. Supported: EUR, USD, GBP. + # * :description -- description to be pased to the gateway. + def purchase(money, creditcard, options = {}) + requires!(options, :order_id) + + post = { 'Descripcion' => options[:description], + 'Num_operacion' => options[:order_id], + 'Idioma' => CECA_UI_LESS_LANGUAGE, + 'Pago_soportado' => CECA_MODE, + 'URL_OK' => CECA_NOTIFICATIONS_URL, + 'URL_NOK' => CECA_NOTIFICATIONS_URL, + 'Importe' => amount(money), + 'TipoMoneda' => CECA_CURRENCIES_DICTIONARY[options[:currency] || currency(money)] } + + add_creditcard(post, creditcard) + + commit(CECA_ACTION_PURCHASE, post) + end + + # Refund a transaction. + # + # This transaction indicates to the gateway that + # money should flow from the merchant to the customer. + # + # ==== Parameters + # + # * money -- The amount to be credited to the customer as an Integer value in cents. + # * identification -- The reference given from the gateway on purchase (reference, not operation). + # * options -- A hash of parameters. + def refund(money, identification, options = {}) + reference, order_id = split_authorization(identification) + + post = { 'Referencia' => reference, + 'Num_operacion' => order_id, + 'Idioma' => CECA_UI_LESS_LANGUAGE_REFUND, + 'Pagina' => CECA_UI_LESS_REFUND_PAGE, + 'Importe' => amount(money), + 'TipoMoneda' => CECA_CURRENCIES_DICTIONARY[options[:currency] || currency(money)] } + + commit(CECA_ACTION_REFUND, post) + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r((&?pan=)[^&]*)i, '\1[FILTERED]'). + gsub(%r((&?cvv2=)[^&]*)i, '\1[FILTERED]') + end + + private + + def add_creditcard(post, creditcard) + post['PAN'] = creditcard.number + post['Caducidad'] = expdate(creditcard) + post['CVV2'] = creditcard.verification_value + post['Pago_elegido'] = CECA_MODE + end + + def expdate(creditcard) + "#{format(creditcard.year, :four_digits)}#{format(creditcard.month, :two_digits)}" + end + + def parse(body) + response = {} + + root = REXML::Document.new(body).root + + response[:success] = (root.attributes['valor'] == 'OK') + response[:date] = root.attributes['fecha'] + response[:operation_number] = root.attributes['numeroOperacion'] + response[:message] = root.attributes['valor'] + + if root.elements['OPERACION'] + response[:operation_type] = root.elements['OPERACION'].attributes['tipo'] + response[:amount] = root.elements['OPERACION/importe'].text.strip + end + + response[:description] = root.elements['OPERACION/descripcion'].text if root.elements['OPERACION/descripcion'] + response[:authorization_number] = root.elements['OPERACION/numeroAutorizacion'].text if root.elements['OPERACION/numeroAutorizacion'] + response[:reference] = root.elements['OPERACION/referencia'].text if root.elements['OPERACION/referencia'] + response[:pan] = root.elements['OPERACION/pan'].text if root.elements['OPERACION/pan'] + + if root.elements['ERROR'] + response[:error_code] = root.elements['ERROR/codigo'].text + response[:error_message] = root.elements['ERROR/descripcion'].text + elsif root.elements['OPERACION'].attributes['numeroOperacion'] == '000' + response[:authorization] = root.elements['OPERACION/numeroAutorizacion'].text if root.elements['OPERACION/numeroAutorizacion'] + else + response[:authorization] = root.attributes['numeroOperacion'] + end + + return response + rescue REXML::ParseException => e + response[:success] = false + response[:message] = 'Unable to parse the response.' + response[:error_message] = e.message + response + end + + def commit(action, parameters) + parameters.merge!( + 'Cifrado' => CECA_ENCRIPTION, + 'Firma' => generate_signature(action, parameters), + 'Exponente' => CECA_DECIMALS, + 'MerchantID' => options[:merchant_id], + 'AcquirerBIN' => options[:acquirer_bin], + 'TerminalID' => options[:terminal_id] + ) + url = (test? ? self.test_url : self.live_url) + "/tpvweb/#{action}.action" + xml = ssl_post("#{url}?", post_data(parameters)) + response = parse(xml) + Response.new( + response[:success], + message_from(response), + response, + test: test?, + authorization: build_authorization(response), + error_code: response[:error_code] + ) + end + + def message_from(response) + if response[:message] == 'ERROR' && response[:error_message] + response[:error_message] + elsif response[:error_message] + "#{response[:message]} #{response[:error_message]}" + else + response[:message] + end + end + + def post_data(params) + return nil unless params + + params.map do |key, value| + next if value.blank? + + if value.is_a?(Hash) + h = {} + value.each do |k, v| + h["#{key}.#{k}"] = v unless v.blank? + end + post_data(h) + else + "#{key}=#{CGI.escape(value.to_s)}" + end + end.compact.join('&') + end + + def build_authorization(response) + [response[:reference], response[:authorization]].join('|') + end + + def split_authorization(authorization) + authorization.split('|') + end + + def generate_signature(action, parameters) + signature_fields = + case action + when CECA_ACTION_REFUND + options[:signature_key].to_s + + options[:merchant_id].to_s + + options[:acquirer_bin].to_s + + options[:terminal_id].to_s + + parameters['Num_operacion'].to_s + + parameters['Importe'].to_s + + parameters['TipoMoneda'].to_s + + CECA_DECIMALS + + parameters['Referencia'].to_s + + CECA_ENCRIPTION + else + options[:signature_key].to_s + + options[:merchant_id].to_s + + options[:acquirer_bin].to_s + + options[:terminal_id].to_s + + parameters['Num_operacion'].to_s + + parameters['Importe'].to_s + + parameters['TipoMoneda'].to_s + + CECA_DECIMALS + + CECA_ENCRIPTION + + CECA_NOTIFICATIONS_URL + + CECA_NOTIFICATIONS_URL + end + Digest::SHA2.hexdigest(signature_fields) + 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 bfefa82ce19..28cd1014a0c 100644 --- a/lib/active_merchant/billing/gateways/checkout_v2.rb +++ b/lib/active_merchant/billing/gateways/checkout_v2.rb @@ -17,14 +17,8 @@ class CheckoutV2Gateway < Gateway TEST_ACCESS_TOKEN_URL = 'https://access.sandbox.checkout.com/connect/token' def initialize(options = {}) - @options = options - @access_token = nil - begin - requires!(options, :secret_key) - rescue ArgumentError - requires!(options, :client_id, :client_secret) - @access_token = setup_access_token - end + options.has_key?(:secret_key) ? requires!(options, :secret_key) : requires!(options, :client_id, :client_secret) + super end @@ -32,15 +26,14 @@ 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) - - options[:incremental_authorization] ? commit(:incremental_authorize, post, options[:incremental_authorization]) : commit(:authorize, post) + options[:incremental_authorization] ? commit(:incremental_authorize, post, options, options[:incremental_authorization]) : commit(:authorize, post, options) end def capture(amount, authorization, options = {}) @@ -48,28 +41,30 @@ def capture(amount, authorization, options = {}) 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 = {} - post[:instruction] = {} - post[:instruction][:funds_transfer_type] = options[:funds_transfer_type] || 'FD' 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) + 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 = {}) @@ -78,15 +73,15 @@ 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 = {}) authorize(0, credit_card, options) end - def verify_payment(authorization, option = {}) - commit(:verify_payment, authorization) + def verify_payment(authorization, options = {}) + commit(:verify_payment, nil, options, authorization, :get) end def supports_scrubbing? @@ -99,7 +94,36 @@ def scrub(transcript) gsub(/("number\\":\\")\d+/, '\1[FILTERED]'). gsub(/("cvv\\":\\")\d+/, '\1[FILTERED]'). gsub(/("cryptogram\\":\\")\w+/, '\1[FILTERED]'). - gsub(/(source\\":\{.*\\"token\\":\\")\d+/, '\1[FILTERED]') + gsub(/(source\\":\{.*\\"token\\":\\")\d+/, '\1[FILTERED]'). + gsub(/("token\\":\\")\w+/, '\1[FILTERED]'). + gsub(/("access_token\\?"\s*:\s*\\?")[^"]*\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 @@ -109,12 +133,17 @@ def build_auth_or_purchase(post, amount, payment_method, 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, payment_method) add_processing_channel(post, options) add_marketplace_data(post, options) + add_recipient_data(post, options) + add_processing_data(post, options) + add_payment_sender_data(post, options) end def add_invoice(post, money, options) @@ -130,6 +159,72 @@ def add_invoice(post, money, options) post[:metadata][:udf5] = application_id || 'ActiveMerchant' end + def add_recipient_data(post, options) + return unless options[:recipient].is_a?(Hash) + + recipient = options[:recipient] + + post[:recipient] = {} + post[:recipient][:dob] = recipient[:dob] if recipient[:dob] + post[:recipient][:zip] = recipient[:zip] if recipient[:zip] + post[:recipient][:account_number] = recipient[:account_number] if recipient[:account_number] + post[:recipient][:first_name] = recipient[:first_name] if recipient[:first_name] + post[:recipient][:last_name] = recipient[:last_name] if recipient[:last_name] + + if address = recipient[:address] + address1 = address[:address1] || address[:address_line1] + address2 = address[:address2] || address[:address_line2] + + post[:recipient][:address] = {} + post[:recipient][:address][:address_line1] = address1 if address1 + post[:recipient][:address][:address_line2] = address2 if address2 + post[:recipient][:address][:city] = address[:city] if address[:city] + post[:recipient][:address][:state] = address[:state] if address[:state] + post[:recipient][:address][:zip] = address[:zip] if address[:zip] + post[:recipient][:address][:country] = address[:country] if address[:country] + end + end + + def add_processing_data(post, options) + return unless options[:processing].is_a?(Hash) + + post[:processing] = options[:processing] + end + + def add_payment_sender_data(post, options) + return unless options[:sender].is_a?(Hash) + + sender = options[:sender] + + post[:sender] = {} + post[:sender][:type] = sender[:type] if sender[:type] + post[:sender][:first_name] = sender[:first_name] if sender[:first_name] + post[:sender][:last_name] = sender[:last_name] if sender[:last_name] + post[:sender][:dob] = sender[:dob] if sender[:dob] + post[:sender][:reference] = sender[:reference] if sender[:reference] + post[:sender][:company_name] = sender[:company_name] if sender[:company_name] + + if address = sender[:address] + address1 = address[:address1] || address[:address_line1] + address2 = address[:address2] || address[:address_line2] + + post[:sender][:address] = {} + post[:sender][:address][:address_line1] = address1 if address1 + post[:sender][:address][:address_line2] = address2 if address2 + post[:sender][:address][:city] = address[:city] if address[:city] + post[:sender][:address][:state] = address[:state] if address[:state] + post[:sender][:address][:zip] = address[:zip] if address[:zip] + post[:sender][:address][:country] = address[:country] if address[:country] + end + + if identification = sender[:identification] + post[:sender][:identification] = {} + post[:sender][:identification][:type] = identification[:type] if identification[:type] + post[:sender][:identification][:number] = identification[:number] if identification[:number] + post[:sender][:identification][:issuing_country] = identification[:issuing_country] if identification[:issuing_country] + end + end + def add_authorization_type(post, options) post[:authorization_type] = options[:authorization_type] if options[:authorization_type] end @@ -141,8 +236,10 @@ def add_metadata(post, options, payment_method = nil) end def add_payment_method(post, payment_method, options, key = :source) + # the key = :destination when this method is called in def credit post[key] = {} - if payment_method.is_a?(NetworkTokenizationCreditCard) + case payment_method + when NetworkTokenizationCreditCard token_type = token_type_from(payment_method) cryptogram = payment_method.payment_cryptogram eci = payment_method.eci || options[:eci] @@ -153,23 +250,43 @@ def add_payment_method(post, payment_method, options, key = :source) post[key][:token_type] = token_type post[key][:cryptogram] = cryptogram if cryptogram post[key][:eci] = eci if eci - elsif payment_method.is_a?(CreditCard) + 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 + 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] - 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 + + 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 - unless payment_method.is_a?(String) + 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 @@ -197,6 +314,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 @@ -211,7 +350,7 @@ def merchant_initiated_override(post, options) end def add_stored_credentials_using_normalized_fields(post, options) - if options[:stored_credential][:initial_transaction] == true + if options[:stored_credential][:initiator] == 'cardholder' post[:merchant_initiated] = false else post[:source][:stored] = true @@ -256,6 +395,82 @@ 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) + + if 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 + end + def add_marketplace_data(post, options) if options[:marketplace] post[:marketplace] = {} @@ -274,18 +489,43 @@ def access_token_url test? ? TEST_ACCESS_TOKEN_URL : LIVE_ACCESS_TOKEN_URL end + def expires_date_with_extra_range(expires_in) + # Two minutes are subtracted from the expires_in time to generate the expires date + # in order to prevent any transaction from failing due to using an access_token + # that is very close to expiring. + # e.g. the access_token has one second left to expire and the lag when the transaction + # use an already expired access_token + (DateTime.now + (expires_in - 120).seconds).strftime('%Q').to_i + end + def setup_access_token - request = 'grant_type=client_credentials' - response = parse(ssl_post(access_token_url, request, access_token_header)) - response['access_token'] + response = parse(ssl_post(access_token_url, 'grant_type=client_credentials', access_token_header)) + @options[:access_token] = response['access_token'] + @options[:expires] = expires_date_with_extra_range(response['expires_in']) if response['expires_in'] && response['expires_in'] > 0 + + Response.new( + access_token_valid?, + message_from(access_token_valid?, response, {}), + response.merge({ expires: @options[:expires] }), + test: test?, + error_code: error_code_from(access_token_valid?, response, {}) + ) + rescue ResponseError => e + raise OAuthResponseError.new(e) + end + + def access_token_valid? + @options[:access_token].present? && @options[:expires].to_i > DateTime.now.strftime('%Q').to_i end - def commit(action, post, authorization = nil) + def perform_request(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') rescue ResponseError => e + @options[:access_token] = '' if e.response.code == '401' && !@options[:secret_key] + raise unless e.response.code.to_s =~ /4\d\d/ response = parse(e.response.body, error: e.response) @@ -293,45 +533,68 @@ def commit(action, post, authorization = nil) succeeded = success_from(action, response) - response(action, succeeded, response) + response(action, succeeded, response, options) 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 commit(action, post, options, authorization = nil, method = :post) + MultiResponse.run do |r| + r.process { setup_access_token } unless @options[:secret_key] || access_token_valid? + r.process { perform_request(action, post, options, authorization, method) } + end + end + def response(action, succeeded, response, options = {}, 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), + message_from(succeeded, response, options), + body, + authorization: authorization, + error_code: error_code_from(succeeded, body, options), test: test?, - avs_result: avs_result, - cvv_result: cvv_result + avs_result: avs_result(response), + cvv_result: cvv_result(response) ) end - def headers - auth_token = @access_token ? "Bearer #{@access_token}" : @options[:secret_key] - { + def headers(action, options) + auth_token = @options[:access_token] ? "Bearer #{@options[: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 credit].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" - elsif action == :incremental_authorize + 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 @@ -342,11 +605,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) @@ -363,17 +626,27 @@ def parse(body, error: nil) 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'] + return true if store_response && ((action == :tokens && store_response.match(/tok/)) || (action == :store && store_response.match(/src_/))) response['response_summary'] == 'Approved' || response['approved'] == true || !response.key?('response_summary') && response.key?('action_id') end - def message_from(succeeded, response) + def message_from(succeeded, response, options) if succeeded 'Succeeded' elsif response['error_type'] response['error_type'] + ': ' + response['error_codes'].first else - response['response_summary'] || response['response_code'] || response['status'] || response['message'] || 'Unable to read error message' + response_summary = if options[:threeds_response_message] + response['response_summary'] || response.dig('actions', 0, 'response_summary') + else + response['response_summary'] + end + + response_summary || response['response_code'] || response['status'] || response['message'] || 'Unable to read error message' end end @@ -394,7 +667,7 @@ def authorization_from(raw) raw['id'] end - def error_code_from(succeeded, response) + def error_code_from(succeeded, response, options) return if succeeded if response['error_type'] && response['error_codes'] @@ -402,7 +675,13 @@ def error_code_from(succeeded, response) elsif response['error_type'] response['error_type'] else - STANDARD_ERROR_CODE_MAPPING[response['response_code']] + response_code = if options[:threeds_response_message] + response['response_code'] || response.dig('actions', 0, 'response_code') + else + response['response_code'] + end + + STANDARD_ERROR_CODE_MAPPING[response_code] end end @@ -416,6 +695,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 index 4db00a258e4..3bbd52925cc 100644 --- a/lib/active_merchant/billing/gateways/commerce_hub.rb +++ b/lib/active_merchant/billing/gateways/commerce_hub.rb @@ -18,7 +18,8 @@ class CommerceHubGateway < Gateway 'sale' => '/payments/v1/charges', 'void' => '/payments/v1/cancels', 'refund' => '/payments/v1/refunds', - 'vault' => '/payments-vas/v1/tokens' + 'vault' => '/payments-vas/v1/tokens', + 'verify' => '/payments-vas/v1/accounts/verification' } def initialize(options = {}) @@ -29,7 +30,9 @@ def initialize(options = {}) def purchase(money, payment, options = {}) post = {} options[:capture_flag] = true - add_transaction_details(post, options) + options[:create_token] = false + + add_transaction_details(post, options, 'sale') build_purchase_and_auth_request(post, money, payment, options) commit('sale', post, options) @@ -38,7 +41,9 @@ def purchase(money, payment, options = {}) def authorize(money, payment, options = {}) post = {} options[:capture_flag] = false - add_transaction_details(post, options) + options[:create_token] = false + + add_transaction_details(post, options, 'sale') build_purchase_and_auth_request(post, money, payment, options) commit('sale', post, options) @@ -49,7 +54,8 @@ def capture(money, authorization, options = {}) options[:capture_flag] = true add_invoice(post, money, options) add_transaction_details(post, options, 'capture') - add_reference_transaction_details(post, authorization, options, 'capture') + add_reference_transaction_details(post, authorization, options, :capture) + add_dynamic_descriptors(post, options) commit('sale', post, options) end @@ -58,7 +64,16 @@ 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) + add_reference_transaction_details(post, authorization, options, :refund) + + commit('refund', post, options) + end + + def credit(money, payment_method, options = {}) + post = {} + add_invoice(post, money, options) + add_transaction_interaction(post, options) + add_payment(post, payment_method, options) commit('refund', post, options) end @@ -66,7 +81,7 @@ def refund(money, authorization, options = {}) def void(authorization, options = {}) post = {} add_transaction_details(post, options) - add_reference_transaction_details(post, authorization, options) + add_reference_transaction_details(post, authorization, options, :void) commit('void', post, options) end @@ -82,10 +97,11 @@ def store(credit_card, options = {}) end def verify(credit_card, options = {}) - verify_amount = options[:verify_amount] || 0 - options[:primary_transaction_type] = 'AUTH_ONLY' - options[:account_verification] = true - authorize(verify_amount, 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? @@ -103,21 +119,52 @@ def scrub(transcript) private + def add_three_d_secure(post, payment, options) + return unless three_d_secure = options[:three_d_secure] + + post[:additionalData3DS] = { + dsTransactionId: three_d_secure[:ds_transaction_id], + authenticationStatus: three_d_secure[:authentication_response_status], + serviceProviderTransactionId: three_d_secure[:three_ds_server_trans_id], + acsTransactionId: three_d_secure[:acs_transaction_id], + mpiData: { + cavv: three_d_secure[:cavv], + eci: three_d_secure[:eci], + xid: three_d_secure[:xid] + }.compact, + versionData: { recommendedVersion: three_d_secure[:version] } + }.compact + end + def add_transaction_interaction(post, options) post[:transactionInteraction] = {} - post[:transactionInteraction][:origin] = options[:transaction_origin] || 'ECOM' + 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) - post[:transactionDetails] = {} - post[:transactionDetails][:captureFlag] = options[:capture_flag] unless options[:capture_flag].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' - post[:transactionDetails][:merchantInvoiceNumber] = options[:merchant_invoice_number] || rand.to_s[2..13] - post[:transactionDetails][:primaryTransactionType] = options[:primary_transaction_type] if options[:primary_transaction_type] - post[:transactionDetails][:accountVerification] = options[:account_verification] unless options[:account_verification].nil? + 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) @@ -167,21 +214,37 @@ def add_shipping_address(post, options) end def build_purchase_and_auth_request(post, money, payment, options) + add_three_d_secure(post, 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) + add_dynamic_descriptors(post, options) + end + + def add_dynamic_descriptors(post, options) + dynamic_descriptors_fields = %i[mcc merchant_name customer_service_number service_entitlement dynamic_descriptors_address] + return unless dynamic_descriptors_fields.any? { |key| options.include?(key) } + + dynamic_descriptors = {} + dynamic_descriptors[:mcc] = options[:mcc] if options[:mcc] + dynamic_descriptors[:merchantName] = options[:merchant_name] if options[:merchant_name] + dynamic_descriptors[:customerServiceNumber] = options[:customer_service_number] if options[:customer_service_number] + dynamic_descriptors[:serviceEntitlement] = options[:service_entitlement] if options[:service_entitlement] + dynamic_descriptors[:address] = options[:dynamic_descriptors_address] if options[:dynamic_descriptors_address] + + post[:dynamicDescriptors] = dynamic_descriptors end def add_reference_transaction_details(post, authorization, options, action = nil) - post[:referenceTransactionDetails] = {} - post[:referenceTransactionDetails][:referenceTransactionId] = authorization - if action != 'capture' - post[:referenceTransactionDetails][:referenceTransactionType] = options[:reference_transaction_type] || 'CHARGES' - post[:referenceTransactionDetails][:referenceMerchantTransactionId] = options[:reference_merchant_transaction_id] - end + 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) @@ -198,7 +261,7 @@ def add_stored_credentials(post, options) 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] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id] + post[:storedCredentials][:schemeReferenceTransactionId] = options[:scheme_reference_transaction_id] || stored_credential[:network_transaction_id] end def add_credit_card(source, payment, options) @@ -240,7 +303,12 @@ def add_payment(post, payment, options = {}) when NetworkTokenizationCreditCard add_decrypted_wallet(source, payment, options) when CreditCard - add_credit_card(source, payment, options) + 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 @@ -257,7 +325,7 @@ def headers(request, options) 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], @@ -267,7 +335,7 @@ def headers(request, options) 'Content-Type' => 'application/json', 'Accept' => 'application/json', 'Authorization' => signature - } + }.merge!(custom_headers) end def add_merchant_details(post) @@ -282,12 +350,25 @@ def commit(action, parameters, options) response = parse(ssl_post(url, parameters.to_json, headers(parameters.to_json, options))) Response.new( - success_from(response), - message_from(response), + success_from(response, action), + message_from(response, action), response, - authorization: authorization_from(action, response), + authorization: authorization_from(action, response, options), test: test?, - error_code: error_code_from(response) + 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 @@ -300,21 +381,32 @@ def handle_response(response) end end - def success_from(response) + 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) - response.dig('paymentReceipt', 'processorResponseDetails', 'responseMessage') || response.dig('error', 0, 'message') || response.dig('gatewayResponse', 'transactionType') + 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) - return response.dig('gatewayResponse', 'transactionProcessingDetails', 'transactionId') unless action == 'vault' - return response.dig('paymentTokens', 0, 'tokenData') if action == 'vault' + 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) - response.dig('error', 0, 'type') unless success_from(response) + def error_code_from(response, action) + response.dig('error', 0, 'code') unless success_from(response, action) end end end diff --git a/lib/active_merchant/billing/gateways/credorax.rb b/lib/active_merchant/billing/gateways/credorax.rb index 768f47cadc2..80b241616c0 100644 --- a/lib/active_merchant/billing/gateways/credorax.rb +++ b/lib/active_merchant/billing/gateways/credorax.rb @@ -30,7 +30,8 @@ class CredoraxGateway < Gateway NETWORK_TOKENIZATION_CARD_SOURCE = { 'apple_pay' => 'applepay', - 'google_pay' => 'googlepay' + 'google_pay' => 'googlepay', + 'network_token' => 'vts_mdes_token' } RESPONSE_MESSAGES = { @@ -140,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) @@ -156,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) @@ -216,7 +217,7 @@ 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) @@ -253,9 +254,7 @@ def add_3ds_2_optional_fields(post, options) normalized_value = normalize(value) next if normalized_value.nil? - if key == :'3ds_homephonecountry' - next unless options[:billing_address] && options[:billing_address][:phone] - end + next if key == :'3ds_homephonecountry' && !(options[:billing_address] && options[:billing_address][:phone]) post[key] = normalized_value unless post[key] end @@ -282,9 +281,9 @@ def add_invoice(post, money, options) 'maestro' => '9' } - def add_payment_method(post, payment_method) + def add_payment_method(post, payment_method, options) post[:c1] = payment_method&.name || '' - post[:b21] = NETWORK_TOKENIZATION_CARD_SOURCE[payment_method.source.to_s] if payment_method.is_a? NetworkTokenizationCreditCard + 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 @@ -292,6 +291,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 @@ -300,20 +306,16 @@ def add_stored_credential(post, options) if stored_credential[:initiator] == 'merchant' case stored_credential[:reason_type] when 'recurring' - recurring_properties(post, stored_credential) + 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 end - def recurring_properties(post, stored_credential) - post[:a9] = stored_credential[:initial_transaction] ? '1' : '2' - post[:g6] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id] - end - def add_customer_data(post, options) post[:d1] = options[:ip] || '127.0.0.1' if (billing_address = options[:billing_address]) @@ -503,7 +505,7 @@ def url end def parse(body) - Hash[CGI::parse(body).map { |k, v| [k.upcase, v.first] }] + CGI::parse(body).map { |k, v| [k.upcase, v.first] }.to_h end def success_from(response) diff --git a/lib/active_merchant/billing/gateways/cyber_source.rb b/lib/active_merchant/billing/gateways/cyber_source.rb index bc11b577e3c..78cc67b7d5d 100644 --- a/lib/active_merchant/billing/gateways/cyber_source.rb +++ b/lib/active_merchant/billing/gateways/cyber_source.rb @@ -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] @@ -132,6 +141,16 @@ class CyberSourceGateway < Gateway r703: 'Export hostname_country/ip_country match' } + @@wallet_payment_solution = { + apple_pay: '001', + google_pay: '012' + } + + NT_PAYMENT_SOLUTION = { + 'master' => '014', + 'visa' => '015' + } + # These are the options that can be used when creating a new CyberSource # Gateway object. # @@ -156,9 +175,15 @@ def initialize(options = {}) super end - def authorize(money, creditcard_or_reference, options = {}) - setup_address_hash(options) - commit(build_auth_request(money, creditcard_or_reference, options), :authorize, money, options) + def authorize(money, payment_method, options = {}) + if valid_payment_method?(payment_method) + setup_address_hash(options) + commit(build_auth_request(money, payment_method, options), :authorize, money, options) + else + # this is for NetworkToken, ApplePay or GooglePay brands that aren't supported at CyberSource + payment_type = payment_method.source.to_s.gsub('_', ' ').titleize.gsub(' ', '') + Response.new(false, "#{card_brand(payment_method).capitalize} is not supported by #{payment_type} at CyberSource, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html") + end end def capture(money, authorization, options = {}) @@ -166,9 +191,15 @@ def capture(money, authorization, options = {}) commit(build_capture_request(money, authorization, options), :capture, money, options) end - def purchase(money, payment_method_or_reference, options = {}) - setup_address_hash(options) - commit(build_purchase_request(money, payment_method_or_reference, options), :purchase, money, options) + def purchase(money, payment_method, options = {}) + if valid_payment_method?(payment_method) + setup_address_hash(options) + commit(build_purchase_request(money, payment_method, options), :purchase, money, options) + else + # this is for NetworkToken, ApplePay or GooglePay brands that aren't supported at CyberSource + payment_type = payment_method.source.to_s.gsub('_', ' ').titleize.gsub(' ', '') + Response.new(false, "#{card_brand(payment_method).capitalize} is not supported by #{payment_type} at CyberSource, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html") + end end def void(identification, options = {}) @@ -201,8 +232,14 @@ def credit(money, creditcard_or_reference, options = {}) # To charge the card while creating a profile, pass # options[:setup_fee] => money def store(payment_method, options = {}) - setup_address_hash(options) - commit(build_create_subscription_request(payment_method, options), :store, nil, options) + if valid_payment_method?(payment_method) + setup_address_hash(options) + commit(build_create_subscription_request(payment_method, options), :store, nil, options) + else + # this is for NetworkToken, ApplePay or GooglePay brands that aren't supported at CyberSource + payment_type = payment_method.source.to_s.gsub('_', ' ').titleize.gsub(' ', '') + Response.new(false, "#{card_brand(payment_method).capitalize} is not supported by #{payment_type} at CyberSource, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html") + end end # Updates a customer subscription/profile @@ -267,6 +304,8 @@ def scrub(transcript) gsub(%r(()[^<]*())i, '\1[FILTERED]\2'). gsub(%r(()[^<]*())i, '\1[FILTERED]\2'). gsub(%r(()[^<]*())i, '\1[FILTERED]\2'). + gsub(%r(()[^<]*())i, '\1[FILTERED]\2'). + gsub(%r(()[^<]*())i, '\1[FILTERED]\2'). gsub(%r(()[^<]*())i, '\1[FILTERED]\2') end @@ -281,6 +320,12 @@ def verify_credentials private + def valid_payment_method?(payment_method) + return true unless payment_method.is_a?(NetworkTokenizationCreditCard) + + %w(visa master american_express).include?(card_brand(payment_method)) + end + # Create all required address hash key value pairs # If a value of nil is received, that value will be passed on to the gateway and will not be replaced with a default value # Billing address fields received without an override value or with an empty string value will be replaced with the default_address values @@ -312,16 +357,19 @@ def build_auth_request(money, creditcard_or_reference, options) xml = Builder::XmlMarkup.new indent: 2 add_customer_id(xml, options) add_payment_method_or_subscription(xml, money, creditcard_or_reference, options) - add_other_tax(xml, options) add_threeds_2_ucaf_data(xml, creditcard_or_reference, options) + add_mastercard_network_tokenization_ucaf_data(xml, creditcard_or_reference, options) add_decision_manager_fields(xml, options) + add_other_tax(xml, options) add_mdd_fields(xml, options) add_auth_service(xml, creditcard_or_reference, options) + add_capture_service_fields_with_run_false(xml, options) add_threeds_services(xml, options) 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_network_token(xml, creditcard_or_reference, options) + add_payment_solution(xml, creditcard_or_reference) add_tax_management_indicator(xml, options) add_stored_credential_subsequent_auth(xml, options) add_issuer_additional_data(xml, options) @@ -374,9 +422,10 @@ def build_purchase_request(money, payment_method_or_reference, options) xml = Builder::XmlMarkup.new indent: 2 add_customer_id(xml, options) add_payment_method_or_subscription(xml, money, payment_method_or_reference, options) - add_other_tax(xml, options) add_threeds_2_ucaf_data(xml, payment_method_or_reference, options) + add_mastercard_network_tokenization_ucaf_data(xml, payment_method_or_reference, options) add_decision_manager_fields(xml, options) + add_other_tax(xml, options) add_mdd_fields(xml, options) 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) @@ -392,7 +441,8 @@ def build_purchase_request(money, payment_method_or_reference, options) 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_network_token(xml, payment_method_or_reference, options) + add_payment_solution(xml, payment_method_or_reference) add_tax_management_indicator(xml, options) add_stored_credential_subsequent_auth(xml, options) add_issuer_additional_data(xml, options) @@ -480,7 +530,7 @@ def build_create_subscription_request(payment_method, options) add_check_service(xml) else add_purchase_service(xml, payment_method, options) - add_payment_network_token(xml) if network_tokenization?(payment_method) + add_payment_network_token(xml, payment_method, options) end end add_subscription_create_service(xml, options) @@ -519,11 +569,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 @@ -551,7 +599,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 @@ -672,6 +720,18 @@ def add_decision_manager_fields(xml, options) end end + def add_payment_solution(xml, payment_method) + return unless network_tokenization?(payment_method) + + case payment_method.source + when :network_token + payment_solution = NT_PAYMENT_SOLUTION[payment_method.brand] + xml.tag! 'paymentSolution', payment_solution if payment_solution + when :apple_pay, :google_pay + xml.tag! 'paymentSolution', @@wallet_payment_solution[payment_method.source] + end + end + def add_issuer_additional_data(xml, options) return unless options[:issuer_additional_data] @@ -681,7 +741,7 @@ def add_issuer_additional_data(xml, options) end def add_other_tax(xml, options) - return unless options[:local_tax_amount] || options[:national_tax_amount] || options[:national_tax_indicator] + return unless %i[vat_tax_rate local_tax_amount national_tax_amount national_tax_indicator].any? { |gsf| options.include?(gsf) } xml.tag! 'otherTax' do xml.tag! 'vatTaxRate', options[:vat_tax_rate] if options[:vat_tax_rate] @@ -705,8 +765,8 @@ def add_mdd_fields(xml, options) 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 @@ -720,21 +780,39 @@ def add_tax_service(xml) def add_auth_service(xml, payment_method, options) if network_tokenization?(payment_method) - add_auth_network_tokenization(xml, payment_method, options) + if payment_method.source == :network_token + add_auth_network_tokenization(xml, payment_method, options) + else + add_auth_wallet(xml, payment_method, options) + end else 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!('aggregatorID', options[:aggregator_id]) if options[:aggregator_id] xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] + xml.tag!('firstRecurringPayment', options[:first_recurring_payment]) if options[:first_recurring_payment] 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 @@ -796,26 +874,37 @@ 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 @@wallet_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) + xml.tag! 'ccAuthService', { 'run' => 'true' } do + xml.tag!('networkTokenCryptogram', payment_method.payment_cryptogram) + xml.tag!('commerceIndicator', 'internet') + xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] + end + end + + def add_auth_wallet(xml, payment_method, options) + 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!('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 @@ -826,14 +915,28 @@ def add_auth_network_tokenization(xml, payment_method, options) 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 - def add_payment_network_token(xml) + def add_mastercard_network_tokenization_ucaf_data(xml, payment_method, options) + return unless network_tokenization?(payment_method) && card_brand(payment_method).to_sym == :master + return if payment_method.source == :network_token + + commerce_indicator = 'internet' if subsequent_nt_apple_pay_auth(payment_method.source, options) + + xml.tag! 'ucaf' do + xml.tag!('authenticationData', payment_method.payment_cryptogram) unless commerce_indicator + xml.tag!('collectionIndicator', DEFAULT_COLLECTION_INDICATOR) + end + end + + def add_payment_network_token(xml, payment_method, options) + return unless network_tokenization?(payment_method) + + transaction_type = payment_method.source == :network_token ? '3' : '1' xml.tag! 'paymentNetworkToken' do - xml.tag!('transactionType', '1') + xml.tag!('requestorID', options[:trid]) if transaction_type == '3' && options[:trid] + xml.tag!('transactionType', transaction_type) end end @@ -846,10 +949,19 @@ def add_capture_service(xml, request_id, request_token, options) end end + def add_capture_service_fields_with_run_false(xml, options) + return unless options[:gratuity_amount] + + xml.tag! 'ccCaptureService', { 'run' => 'false' } do + xml.tag! 'gratuityAmount', options[:gratuity_amount] + end + end + def add_purchase_service(xml, payment_method, options) add_auth_service(xml, payment_method, options) xml.tag! 'ccCaptureService', { 'run' => 'true' } do xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] + xml.tag!('gratuityAmount', options[:gratuity_amount]) if options[:gratuity_amount] end end @@ -994,7 +1106,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) @@ -1005,6 +1117,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 @@ -1055,12 +1179,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 @@ -1094,6 +1232,7 @@ def parse_element(reply, node) parent += '_' + node.parent.attributes['id'] if node.parent.attributes['id'] parent += '_' end + reply[:reconciliationID2] = node.text if node.name == 'reconciliationID' && reply[:reconciliationID] reply["#{parent}#{node.name}".to_sym] ||= node.text end return reply @@ -1131,6 +1270,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..8aa79675947 --- /dev/null +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -0,0 +1,497 @@ +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' + } + + WALLET_PAYMENT_SOLUTION = { + apple_pay: '001', + google_pay: '012' + } + + NT_PAYMENT_SOLUTION = { + 'master' => '014', + 'visa' => '015' + } + + 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(/(\\?"cryptogram\\?":\\?")[^<]+/, '\1[FILTERED]'). + gsub(/(signature=")[^"]*/, '\1[FILTERED]'). + gsub(/(keyid=")[^"]*/, '\1[FILTERED]'). + gsub(/(Digest: SHA-256=)[\w\/\+=]*/, '\1[FILTERED]') + end + + private + + def add_level_2_data(post, options) + return unless options[:purchase_order_number] + + post[:orderInformation][:invoiceDetails] ||= {} + post[:orderInformation][:invoiceDetails][:purchaseOrderNumber] = options[:purchase_order_number] + end + + def add_level_3_data(post, options) + return unless options[:line_items] + + post[:orderInformation][:lineItems] = options[:line_items] + post[:processingInformation][:purchaseLevel] = '3' + post[:orderInformation][:shipping_details] = { shipFromPostalCode: options[:ships_from_postal_code] } + post[:orderInformation][:amountDetails] ||= {} + post[:orderInformation][:amountDetails][:discountAmount] = options[:discount_amount] + end + + def add_three_ds(post, payment_method, options) + return unless three_d_secure = options[:three_d_secure] + + post[:consumerAuthenticationInformation] ||= {} + if payment_method.brand == 'master' + post[:consumerAuthenticationInformation][:ucafAuthenticationData] = three_d_secure[:cavv] + post[:consumerAuthenticationInformation][:ucafCollectionIndicator] = '2' + else + post[:consumerAuthenticationInformation][:cavv] = three_d_secure[:cavv] + end + post[:consumerAuthenticationInformation][:cavvAlgorithm] = three_d_secure[:cavv_algorithm] if three_d_secure[:cavv_algorithm] + post[:consumerAuthenticationInformation][:paSpecificationVersion] = three_d_secure[:version] if three_d_secure[:version] + post[:consumerAuthenticationInformation][:directoryServerTransactionID] = three_d_secure[:ds_transaction_id] if three_d_secure[:ds_transaction_id] + post[:consumerAuthenticationInformation][:eciRaw] = three_d_secure[:eci] if three_d_secure[:eci] + if three_d_secure[:xid].present? + post[:consumerAuthenticationInformation][:xid] = three_d_secure[:xid] + else + post[:consumerAuthenticationInformation][:xid] = three_d_secure[:cavv] + end + post[:consumerAuthenticationInformation][:veresEnrolled] = three_d_secure[:enrolled] if three_d_secure[:enrolled] + post[:consumerAuthenticationInformation][:paresStatus] = three_d_secure[:authentication_response_status] if three_d_secure[:authentication_response_status] + post + end + + 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) + add_three_ds(post, payment, options) + add_level_2_data(post, options) + add_level_3_data(post, 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][:commerceIndicator] = 'internet' unless options[:stored_credential] || card_brand(payment) == 'jcb' + + post[:paymentInformation][:tokenizedCard] = { + number: payment.number, + expirationMonth: payment.month, + expirationYear: payment.year, + cryptogram: payment.payment_cryptogram, + type: CREDIT_CARD_CODES[card_brand(payment).to_sym], + transactionType: payment.source == :network_token ? '3' : '1' + } + + if payment.source == :network_token && NT_PAYMENT_SOLUTION[payment.brand] + post[:processingInformation][:paymentSolution] = NT_PAYMENT_SOLUTION[payment.brand] + else + # Apple Pay / Google Pay + post[:processingInformation][:paymentSolution] = WALLET_PAYMENT_SOLUTION[payment.source] + if card_brand(payment) == 'master' + post[:consumerAuthenticationInformation] = { + ucafAuthenticationData: payment.payment_cryptogram, + ucafCollectionIndicator: '2' + } + else + post[:consumerAuthenticationInformation] = { cavv: payment.payment_cryptogram } + end + 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 options[:stored_credential] + + post[:processingInformation][:commerceIndicator] = commerce_indicator(options.dig(:stored_credential, :reason_type)) + add_authorization_options(post, payment, options) + end + + def commerce_indicator(reason_type) + case reason_type + when 'recurring' + 'recurring' + when 'installment' + 'install' + else + 'internet' + end + end + + def add_authorization_options(post, payment, options) + initiator = options.dig(:stored_credential, :initiator) == 'cardholder' ? 'customer' : 'merchant' + authorization_options = { + authorizationOptions: { + initiator: { + type: initiator + } + } + }.compact + + authorization_options[:authorizationOptions][:initiator][:storedCredentialUsed] = true if initiator == 'merchant' + authorization_options[:authorizationOptions][:initiator][:credentialStoredOnFile] = true if options.dig(:stored_credential, :initial_transaction) + authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction] ||= {} + unless options.dig(:stored_credential, :initial_transaction) + network_transaction_id = options[:network_transaction_id] || options.dig(:stored_credential, :network_transaction_id) || '' + authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction][:previousTransactionID] = network_transaction_id + authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction][:originalAuthorizedAmount] = post.dig(:orderInformation, :amountDetails, :totalAmount) if card_brand(payment) == 'discover' + end + authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction][:reason] = options[:reason_code] if options[:reason_code] + post[:processingInformation].merge!(authorization_options) + 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] ||= {} + 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 2f0b959f592..c98d551ceec 100644 --- a/lib/active_merchant/billing/gateways/d_local.rb +++ b/lib/active_merchant/billing/gateways/d_local.rb @@ -163,23 +163,19 @@ def add_card(post, card, action, options = {}) 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 + if options[:stored_credential] + # 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') + post[:card][:network_payment_reference] = options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id] + # used case of Network Token: 'CARD_ON_FILE', 'SUBSCRIPTION', 'UNSCHEDULED_CARD_ON_FILE' + post[:card][:stored_credential_type] = fetch_stored_credential_type(options[:stored_credential]) + end + post[:card][:holder_name] = card.name post[:card][:expiration_month] = card.month post[:card][:expiration_year] = card.year @@ -188,6 +184,15 @@ def add_card(post, card, action, options = {}) 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 fetch_stored_credential_type(stored_credential) + if stored_credential[:reason_type] == 'unscheduled' + stored_credential[:initiator] == 'merchant' ? 'UNSCHEDULED_CARD_ON_FILE' : 'CARD_ON_FILE' + else + 'SUBSCRIPTION' + end end def parse(body) @@ -217,6 +222,7 @@ def commit(action, parameters, options = {}) message_from(action, response), response, authorization: authorization_from(response), + network_transaction_id: network_transaction_id_from(response), avs_result: AVSResult.new(code: response['some_avs_response_key']), cvv_result: CVVResult.new(response['some_cvv_response_key']), test: test?, @@ -241,6 +247,10 @@ def authorization_from(response) response['id'] end + def network_transaction_id_from(response) + response.dig('card', 'network_tx_reference') + end + def error_code_from(action, response) return if success_from(action, response) @@ -249,7 +259,7 @@ def error_code_from(action, response) end def url(action, parameters, options = {}) - "#{(test? ? test_url : live_url)}/#{endpoint(action, parameters, options)}/" + "#{test? ? test_url : live_url}/#{endpoint(action, parameters, options)}/" end def endpoint(action, parameters, options) 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/datatrans.rb b/lib/active_merchant/billing/gateways/datatrans.rb new file mode 100644 index 00000000000..6d1a3c686d9 --- /dev/null +++ b/lib/active_merchant/billing/gateways/datatrans.rb @@ -0,0 +1,228 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class DatatransGateway < Gateway + self.test_url = 'https://api.sandbox.datatrans.com/v1/transactions/' + self.live_url = 'https://api.datatrans.com/v1/transactions/' + + self.supported_countries = %w(CH GR US) # to confirm the countries supported. + self.default_currency = 'CHF' + self.currencies_without_fractions = %w(CHF EUR USD) + self.currencies_with_three_decimal_places = %w() + self.supported_cardtypes = %i[master visa american_express unionpay diners_club discover jcb maestro dankort] + + self.money_format = :cents + + self.homepage_url = 'https://www.datatrans.ch/' + self.display_name = 'Datatrans' + + CREDIT_CARD_SOURCE = { + visa: 'VISA', + master: 'MASTERCARD' + }.with_indifferent_access + + DEVICE_SOURCE = { + apple_pay: 'APPLE_PAY', + google_pay: 'GOOGLE_PAY' + }.with_indifferent_access + + def initialize(options = {}) + requires!(options, :merchant_id, :password) + @merchant_id, @password = options.values_at(:merchant_id, :password) + super + end + + def purchase(money, payment, options = {}) + authorize(money, payment, options.merge(auto_settle: true)) + end + + def authorize(money, payment, options = {}) + post = { refno: options.fetch(:order_id, '') } + add_payment_method(post, payment) + add_3ds_data(post, payment, options) + add_currency_amount(post, money, options) + add_billing_address(post, options) + post[:autoSettle] = options[:auto_settle] if options[:auto_settle] + commit('authorize', post) + end + + def capture(money, authorization, options = {}) + post = { refno: options.fetch(:order_id, '') } + transaction_id = authorization.split('|').first + add_currency_amount(post, money, options) + commit('settle', post, { transaction_id: transaction_id }) + end + + def refund(money, authorization, options = {}) + post = { refno: options.fetch(:order_id, '') } + transaction_id = authorization.split('|').first + add_currency_amount(post, money, options) + commit('credit', post, { transaction_id: transaction_id }) + end + + def void(authorization, options = {}) + post = {} + transaction_id = authorization.split('|').first + commit('cancel', post, { transaction_id: transaction_id }) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Basic )[\w =]+), '\1[FILTERED]'). + gsub(%r((\"number\\":\\")\d+), '\1[FILTERED]\2'). + gsub(%r((\"cvv\\":\\")\d+), '\1[FILTERED]\2') + end + + private + + def add_payment_method(post, payment_method) + card = build_card(payment_method) + post[:card] = { + expiryMonth: format(payment_method.month, :two_digits), + expiryYear: format(payment_method.year, :two_digits) + }.merge(card) + end + + def build_card(payment_method) + if payment_method.is_a?(NetworkTokenizationCreditCard) + { + type: DEVICE_SOURCE[payment_method.source] ? 'DEVICE_TOKEN' : 'NETWORK_TOKEN', + tokenType: DEVICE_SOURCE[payment_method.source] || CREDIT_CARD_SOURCE[card_brand(payment_method)], + token: payment_method.number, + cryptogram: payment_method.payment_cryptogram + } + else + { + number: payment_method.number, + cvv: payment_method.verification_value.to_s + } + end + end + + def add_3ds_data(post, payment_method, options) + return unless three_d_secure = options[:three_d_secure] + + three_ds = + { + "3D": + { + eci: three_d_secure[:eci], + xid: three_d_secure[:xid], + threeDSTransactionId: three_d_secure[:ds_transaction_id], + cavv: three_d_secure[:cavv], + threeDSVersion: three_d_secure[:version], + cavvAlgorithm: three_d_secure[:cavv_algorithm], + directoryResponse: three_d_secure[:directory_response_status], + authenticationResponse: three_d_secure[:authentication_response_status], + transStatusReason: three_d_secure[:trans_status_reason] + }.compact + } + + post[:card].merge!(three_ds) + end + + def add_billing_address(post, options) + return unless billing_address = options[:billing_address] + + post[:billing] = { + name: billing_address[:name], + street: billing_address[:address1], + street2: billing_address[:address2], + city: billing_address[:city], + country: Country.find(billing_address[:country]).code(:alpha3).value, # pass country alpha 2 to country alpha 3, + phoneNumber: billing_address[:phone], + zipCode: billing_address[:zip], + email: options[:email] + }.compact + end + + def add_currency_amount(post, money, options) + post[:currency] = (options[:currency] || currency(money)) + post[:amount] = amount(money) + end + + def commit(action, post, options = {}) + response = parse(ssl_post(url(action, options), post.to_json, headers)) + succeeded = success_from(action, response) + + Response.new( + succeeded, + message_from(succeeded, response), + response, + authorization: authorization_from(response), + test: test?, + error_code: error_code_from(response) + ) + rescue ResponseError => e + response = parse(e.response.body) + Response.new(false, message_from(false, response), response, test: test?, error_code: error_code_from(response)) + end + + def parse(response) + JSON.parse response + rescue JSON::ParserError + msg = 'Invalid JSON response received from Datatrans. Please contact them for support if you continue to receive this message.' + msg += " (The raw response returned by the API was #{response.inspect})" + { + 'successful' => false, + 'response' => {}, + 'errors' => [msg] + } + end + + def headers + { + 'Content-Type' => 'application/json; charset=UTF-8', + 'Authorization' => "Basic #{Base64.strict_encode64("#{@merchant_id}:#{@password}")}" + } + end + + def url(endpoint, options = {}) + case endpoint + when 'settle', 'credit', 'cancel' + "#{test? ? test_url : live_url}#{options[:transaction_id]}/#{endpoint}" + else + "#{test? ? test_url : live_url}#{endpoint}" + end + end + + def success_from(action, response) + case action + when 'authorize', 'credit' + true if response.include?('transactionId') && response.include?('acquirerAuthorizationCode') + when 'settle', 'cancel' + true if response.dig('response_code') == 204 + else + false + end + end + + def authorization_from(response) + auth = [response['transactionId'], response['acquirerAuthorizationCode']].join('|') + return auth unless auth == '|' + end + + def message_from(succeeded, response) + return if succeeded + + response.dig('error', 'message') + end + + def error_code_from(response) + response.dig('error', 'code') + end + + def handle_response(response) + case response.code.to_i + when 200...300 + response.body || { response_code: response.code.to_i }.to_json + else + raise ResponseError.new(response) + end + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/decidir.rb b/lib/active_merchant/billing/gateways/decidir.rb index 38ce71dbab3..58167d5ede8 100644 --- a/lib/active_merchant/billing/gateways/decidir.rb +++ b/lib/active_merchant/billing/gateways/decidir.rb @@ -7,7 +7,7 @@ class DecidirGateway < Gateway self.supported_countries = ['AR'] self.money_format = :cents self.default_currency = 'ARS' - self.supported_cardtypes = %i[visa master american_express diners_club naranja cabal] + self.supported_cardtypes = %i[visa master american_express diners_club naranja cabal tuya] self.homepage_url = 'http://www.decidir.com' self.display_name = 'Decidir' @@ -127,6 +127,7 @@ def add_auth_purchase_params(post, money, credit_card, options) add_payment(post, credit_card, options) add_aggregate_data(post, options) if options[:aggregate_data] add_sub_payments(post, options) + add_customer_data(post, options) end def add_payment_method_id(credit_card, options) @@ -167,29 +168,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) @@ -215,6 +242,14 @@ def add_aggregate_data(post, options) post[:aggregate_data] = aggregate_data end + def add_customer_data(post, options = {}) + return unless options[:customer_email] || options[:customer_id] + + post[:customer] = {} + post[:customer][:id] = options[:customer_id] if options[:customer_id] + post[:customer][:email] = options[:customer_email] if options[:customer_email] + end + def add_sub_payments(post, options) # sub_payments field is required for purchase transactions, even if empty post[:sub_payments] = [] @@ -262,7 +297,7 @@ def headers(options = {}) end def commit(method, endpoint, parameters, options = {}) - url = "#{(test? ? test_url : live_url)}/#{endpoint}" + url = "#{test? ? test_url : live_url}/#{endpoint}" begin raw_response = ssl_request(method, url, post_data(parameters), headers(options)) 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 11e5bb68945..4588eddb7f7 100644 --- a/lib/active_merchant/billing/gateways/ebanx.rb +++ b/lib/active_merchant/billing/gateways/ebanx.rb @@ -4,30 +4,24 @@ 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' TAGS = ['Spreedly'] - CARD_BRAND = { - visa: 'visa', - master: 'master_card', - american_express: 'amex', - discover: 'discover', - diners_club: 'diners' - } - URL_MAP = { purchase: 'direct', authorize: 'direct', capture: 'capture', refund: 'refund', void: 'cancel', - store: 'token' + store: 'token', + inquire: 'query', + verify: 'verifycard' } HTTP_METHOD = { @@ -36,16 +30,9 @@ class EbanxGateway < Gateway capture: :get, refund: :post, void: :get, - store: :post - } - - VERIFY_AMOUNT_PER_COUNTRY = { - 'br' => 100, - 'ar' => 100, - 'co' => 50000, - 'pe' => 300, - 'mx' => 2000, - 'cl' => 80000 + store: :post, + inquire: :get, + verify: :post } def initialize(options = {}) @@ -113,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? @@ -153,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 @@ -189,15 +189,14 @@ def add_invoice(post, money, options) end def add_card_or_token(post, payment, options) - 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] + 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) @@ -235,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?, @@ -257,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 @@ -298,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 @@ -319,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/elavon.rb b/lib/active_merchant/billing/gateways/elavon.rb index 3085354dc8d..f7f5e678575 100644 --- a/lib/active_merchant/billing/gateways/elavon.rb +++ b/lib/active_merchant/billing/gateways/elavon.rb @@ -43,12 +43,7 @@ def purchase(money, payment_method, options = {}) xml.ssl_transaction_type self.actions[:purchase] xml.ssl_amount amount(money) - if payment_method.is_a?(String) - add_token(xml, payment_method) - else - add_creditcard(xml, payment_method) - end - + add_payment(xml, payment_method, options) add_invoice(xml, options) add_salestax(xml, options) add_currency(xml, money, options) @@ -62,15 +57,14 @@ def purchase(money, payment_method, options = {}) commit(request) end - def authorize(money, creditcard, options = {}) + def authorize(money, payment_method, options = {}) request = build_xml_request do |xml| xml.ssl_vendor_id @options[:ssl_vendor_id] || options[:ssl_vendor_id] xml.ssl_transaction_type self.actions[:authorize] xml.ssl_amount amount(money) - add_salestax(xml, options) add_invoice(xml, options) - add_creditcard(xml, creditcard) + add_payment(xml, payment_method, options) add_currency(xml, money, options) add_address(xml, options) add_customer_email(xml, options) @@ -200,6 +194,16 @@ def scrub(transcript) private + def add_payment(xml, payment, options) + if payment.is_a?(String) + xml.ssl_token payment + elsif payment.is_a?(NetworkTokenizationCreditCard) + add_network_token(xml, payment) + else + add_creditcard(xml, payment) + end + end + def add_invoice(xml, options) xml.ssl_invoice_number url_encode_truncate((options[:order_id] || options[:invoice]), 25) xml.ssl_description url_encode_truncate(options[:description], 255) @@ -213,6 +217,16 @@ def add_txn_id(xml, authorization) xml.ssl_txn_id authorization.split(';').last end + def add_network_token(xml, payment_method) + payment = payment_method.payment_data&.gsub('=>', ':') + case payment_method.source + when :apple_pay + xml.ssl_applepay_web url_encode(payment) + when :google_pay + xml.ssl_google_pay url_encode(payment) + end + end + def add_creditcard(xml, creditcard) xml.ssl_card_number creditcard.number xml.ssl_exp_date expdate(creditcard) @@ -451,8 +465,8 @@ def url_encode(value) if value.is_a?(String) encoded = CGI.escape(value) encoded = encoded.tr('+', ' ') # don't encode spaces - encoded = encoded.gsub('%26', '%26amp;') # account for Elavon's weird '&' handling - encoded + encoded.gsub('%26', '%26amp;') # account for Elavon's weird '&' handling + else value.to_s end diff --git a/lib/active_merchant/billing/gateways/element.rb b/lib/active_merchant/billing/gateways/element.rb index f9fe19149bf..b685c7bab9c 100644 --- a/lib/active_merchant/billing/gateways/element.rb +++ b/lib/active_merchant/billing/gateways/element.rb @@ -37,6 +37,7 @@ def purchase(money, payment, options = {}) add_transaction(xml, money, options) add_terminal(xml, options) add_address(xml, options) + add_lodging(xml, options) end end @@ -51,6 +52,7 @@ def authorize(money, payment, options = {}) add_transaction(xml, money, options) add_terminal(xml, options) add_address(xml, options) + add_lodging(xml, options) end end @@ -222,16 +224,44 @@ def market_code(money, options) options[:market_code] || 'Default' end + def add_lodging(xml, options) + if lodging = options[:lodging] + xml.extendedParameters do + xml.ExtendedParameters do + xml.Key 'Lodging' + xml.Value('xsi:type' => 'Lodging') do + xml.LodgingAgreementNumber lodging[:agreement_number] if lodging[:agreement_number] + xml.LodgingCheckInDate lodging[:check_in_date] if lodging[:check_in_date] + xml.LodgingCheckOutDate lodging[:check_out_date] if lodging[:check_out_date] + xml.LodgingRoomAmount lodging[:room_amount] if lodging[:room_amount] + xml.LodgingRoomTax lodging[:room_tax] if lodging[:room_tax] + xml.LodgingNoShowIndicator lodging[:no_show_indicator] if lodging[:no_show_indicator] + xml.LodgingDuration lodging[:duration] if lodging[:duration] + xml.LodgingCustomerName lodging[:customer_name] if lodging[:customer_name] + xml.LodgingClientCode lodging[:client_code] if lodging[:client_code] + xml.LodgingExtraChargesDetail lodging[:extra_charges_detail] if lodging[:extra_charges_detail] + xml.LodgingExtraChargesAmounts lodging[:extra_charges_amounts] if lodging[:extra_charges_amounts] + xml.LodgingPrestigiousPropertyCode lodging[:prestigious_property_code] if lodging[:prestigious_property_code] + xml.LodgingSpecialProgramCode lodging[:special_program_code] if lodging[:special_program_code] + xml.LodgingChargeType lodging[:charge_type] if lodging[:charge_type] + end + end + end + end + end + def add_terminal(xml, options) xml.terminal do xml.TerminalID options[:terminal_id] || '01' + xml.TerminalType options[:terminal_type] if options[:terminal_type] xml.CardPresentCode options[:card_present_code] || 'UseDefault' - xml.CardholderPresentCode 'UseDefault' - xml.CardInputCode 'UseDefault' - xml.CVVPresenceCode 'UseDefault' - xml.TerminalCapabilityCode 'UseDefault' - xml.TerminalEnvironmentCode 'UseDefault' + xml.CardholderPresentCode options[:card_holder_present_code] || 'UseDefault' + xml.CardInputCode options[:card_input_code] || 'UseDefault' + xml.CVVPresenceCode options[:cvv_presence_code] || 'UseDefault' + xml.TerminalCapabilityCode options[:terminal_capability_code] || 'UseDefault' + xml.TerminalEnvironmentCode options[:terminal_environment_code] || 'UseDefault' xml.MotoECICode 'NonAuthenticatedSecureECommerceTransaction' + xml.PartialApprovedFlag options[:partial_approved_flag] if options[:partial_approved_flag] end end @@ -240,7 +270,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 @@ -363,7 +393,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/fat_zebra.rb b/lib/active_merchant/billing/gateways/fat_zebra.rb index 91b4e23bb68..0d534db434c 100644 --- a/lib/active_merchant/billing/gateways/fat_zebra.rb +++ b/lib/active_merchant/billing/gateways/fat_zebra.rb @@ -28,6 +28,7 @@ def purchase(money, creditcard, options = {}) add_order_id(post, options) add_ip(post, options) add_metadata(post, options) + add_three_ds(post, options) commit(:post, 'purchases', post) end @@ -41,6 +42,7 @@ def authorize(money, creditcard, options = {}) add_order_id(post, options) add_ip(post, options) add_metadata(post, options) + add_three_ds(post, options) post[:capture] = false @@ -125,16 +127,42 @@ def add_creditcard(post, creditcard, options = {}) def add_extra_options(post, options) extra = {} extra[:ecm] = '32' if options[:recurring] - extra[:cavv] = options[:cavv] || options.dig(:three_d_secure, :cavv) if options[:cavv] || options.dig(:three_d_secure, :cavv) - extra[:xid] = options[:xid] || options.dig(:three_d_secure, :xid) if options[:xid] || options.dig(:three_d_secure, :xid) - extra[:sli] = options[:sli] || options.dig(:three_d_secure, :eci) if options[:sli] || options.dig(:three_d_secure, :eci) extra[:name] = options[:merchant] if options[:merchant] extra[:location] = options[:merchant_location] if options[:merchant_location] extra[:card_on_file] = options.dig(:extra, :card_on_file) if options.dig(:extra, :card_on_file) extra[:auth_reason] = options.dig(:extra, :auth_reason) if options.dig(:extra, :auth_reason) + + unless options[:three_d_secure].present? + extra[:sli] = options[:sli] if options[:sli] + extra[:xid] = options[:xid] if options[:xid] + extra[:cavv] = options[:cavv] if options[:cavv] + end + post[:extra] = extra if extra.any? end + def add_three_ds(post, options) + return unless three_d_secure = options[:three_d_secure] + + post[:extra] = { + sli: three_d_secure[:eci], + xid: three_d_secure[:xid], + cavv: three_d_secure[:cavv], + par: three_d_secure[:authentication_response_status], + ver: formatted_enrollment(three_d_secure[:enrolled]), + threeds_version: three_d_secure[:version], + ds_transaction_id: three_d_secure[:ds_transaction_id] + }.compact + 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 add_order_id(post, options) post[:reference] = options[:order_id] || SecureRandom.hex(15) end 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/first_pay.rb b/lib/active_merchant/billing/gateways/first_pay.rb index e6f92b3cdd8..daec309819e 100644 --- a/lib/active_merchant/billing/gateways/first_pay.rb +++ b/lib/active_merchant/billing/gateways/first_pay.rb @@ -1,181 +1,15 @@ -require 'nokogiri' +require 'active_merchant/billing/gateways/first_pay/first_pay_xml' +require 'active_merchant/billing/gateways/first_pay/first_pay_json' module ActiveMerchant #:nodoc: module Billing #:nodoc: class FirstPayGateway < Gateway - self.live_url = 'https://secure.goemerchant.com/secure/gateway/xmlgateway.aspx' + self.abstract_class = true - self.supported_countries = ['US'] - self.default_currency = 'USD' - self.money_format = :dollars - self.supported_cardtypes = %i[visa master american_express discover] + def self.new(options = {}) + return FirstPayJsonGateway.new(options) if options[:merchant_key] - self.homepage_url = 'http://1stpaygateway.net/' - self.display_name = '1stPayGateway.Net' - - def initialize(options = {}) - requires!(options, :transaction_center_id, :gateway_id) - super - end - - def purchase(money, payment, options = {}) - post = {} - add_invoice(post, money, options) - add_payment(post, payment, options) - add_address(post, payment, options) - add_customer_data(post, options) - - commit('sale', post) - end - - def authorize(money, payment, options = {}) - post = {} - add_invoice(post, money, options) - add_payment(post, payment, options) - add_address(post, payment, options) - add_customer_data(post, options) - - commit('auth', post) - end - - def capture(money, authorization, options = {}) - post = {} - add_reference(post, 'settle', money, authorization) - commit('settle', post) - end - - def refund(money, authorization, options = {}) - post = {} - add_reference(post, 'credit', money, authorization) - commit('credit', post) - end - - def void(authorization, options = {}) - post = {} - add_reference(post, 'void', nil, authorization) - commit('void', post) - end - - def supports_scrubbing? - true - end - - def scrub(transcript) - transcript. - gsub(%r((gateway_id)[^<]*())i, '\1[FILTERED]\2'). - gsub(%r((card_number)[^<]*())i, '\1[FILTERED]\2'). - gsub(%r((cvv2)[^<]*())i, '\1[FILTERED]\2') - end - - private - - def add_authentication(post, options) - post[:transaction_center_id] = options[:transaction_center_id] - post[:gateway_id] = options[:gateway_id] - end - - def add_customer_data(post, options) - post[:owner_email] = options[:email] if options[:email] - post[:remote_ip_address] = options[:ip] if options[:ip] - post[:processor_id] = options[:processor_id] if options[:processor_id] - end - - def add_address(post, creditcard, options) - if address = options[:billing_address] || options[:address] - post[:owner_name] = address[:name] - post[:owner_street] = address[:address1] - post[:owner_street2] = address[:address2] if address[:address2] - post[:owner_city] = address[:city] - post[:owner_state] = address[:state] - post[:owner_zip] = address[:zip] - post[:owner_country] = address[:country] - post[:owner_phone] = address[:phone] if address[:phone] - end - end - - def add_invoice(post, money, options) - post[:order_id] = options[:order_id] - post[:total] = amount(money) - end - - def add_payment(post, payment, options) - post[:card_name] = payment.brand # Unclear if need to map to known names or open text field?? - post[:card_number] = payment.number - post[:card_exp] = expdate(payment) - post[:cvv2] = payment.verification_value - post[:recurring] = options[:recurring] if options[:recurring] - post[:recurring_start_date] = options[:recurring_start_date] if options[:recurring_start_date] - post[:recurring_end_date] = options[:recurring_end_date] if options[:recurring_end_date] - post[:recurring_type] = options[:recurring_type] if options[:recurring_type] - end - - def add_reference(post, action, money, authorization) - post[:"#{action}_amount1"] = amount(money) if money - post[:total_number_transactions] = 1 - post[:reference_number1] = authorization - end - - def parse(xml) - response = {} - - doc = Nokogiri::XML(xml) - doc.root&.xpath('//RESPONSE/FIELDS/FIELD')&.each do |field| - response[field['KEY']] = field.text - end - - response - end - - def commit(action, parameters) - response = parse(ssl_post(live_url, post_data(action, parameters))) - - Response.new( - success_from(response), - message_from(response), - response, - authorization: authorization_from(response), - error_code: error_code_from(response), - test: test? - ) - end - - def success_from(response) - ( - (response['status'] == '1') || - (response['status1'] == '1') - ) - end - - def message_from(response) - # Silly inconsistent gateway. Always make capitalized (but not all caps) - msg = (response['auth_response'] || response['response1']) - msg&.downcase&.capitalize - end - - def error_code_from(response) - response['error'] - end - - def authorization_from(response) - response['reference_number'] || response['reference_number1'] - end - - def post_data(action, parameters = {}) - parameters[:transaction_center_id] = @options[:transaction_center_id] - parameters[:gateway_id] = @options[:gateway_id] - - parameters[:operation_type] = action - - xml = Builder::XmlMarkup.new - xml.instruct! - xml.tag! 'TRANSACTION' do - xml.tag! 'FIELDS' do - parameters.each do |key, value| - xml.tag! 'FIELD', value, { 'KEY' => key } - end - end - end - xml.target! + FirstPayXmlGateway.new(options) end end end diff --git a/lib/active_merchant/billing/gateways/first_pay/first_pay_common.rb b/lib/active_merchant/billing/gateways/first_pay/first_pay_common.rb new file mode 100644 index 00000000000..f74a6609f5d --- /dev/null +++ b/lib/active_merchant/billing/gateways/first_pay/first_pay_common.rb @@ -0,0 +1,15 @@ +module FirstPayCommon + def self.included(base) + base.supported_countries = ['US'] + base.default_currency = 'USD' + base.money_format = :dollars + base.supported_cardtypes = %i[visa master american_express discover] + + base.homepage_url = 'http://1stpaygateway.net/' + base.display_name = '1stPayGateway.Net' + end + + def supports_scrubbing? + true + end +end diff --git a/lib/active_merchant/billing/gateways/first_pay/first_pay_json.rb b/lib/active_merchant/billing/gateways/first_pay/first_pay_json.rb new file mode 100644 index 00000000000..464aad139de --- /dev/null +++ b/lib/active_merchant/billing/gateways/first_pay/first_pay_json.rb @@ -0,0 +1,190 @@ +require 'active_merchant/billing/gateways/first_pay/first_pay_common' + +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class FirstPayJsonGateway < Gateway + include FirstPayCommon + + ACTIONS = { + purchase: 'Sale', + authorize: 'Auth', + capture: 'Settle', + refund: 'Refund', + void: 'Void' + }.freeze + + WALLET_TYPES = { + apple_pay: 'ApplePay', + google_pay: 'GooglePay' + }.freeze + + self.test_url = 'https://secure-v.1stPaygateway.net/secure/RestGW/Gateway/Transaction/' + self.live_url = 'https://secure.1stPaygateway.net/secure/RestGW/Gateway/Transaction/' + + # Creates a new FirstPayJsonGateway + # + # The gateway requires two values for connection to be passed + # in the +options+ hash. + # + # ==== Options + # + # * :merchant_key -- FirstPay's merchant_key (REQUIRED) + # * :processor_id -- FirstPay's processor_id or processorId (REQUIRED) + def initialize(options = {}) + requires!(options, :merchant_key, :processor_id) + super + end + + def purchase(money, payment, options = {}) + post = {} + add_invoice(post, money, options) + add_payment(post, payment, options) + add_address(post, payment, options) + + commit(:purchase, post) + end + + def authorize(money, payment, options = {}) + post = {} + add_invoice(post, money, options) + add_payment(post, payment, options) + add_address(post, payment, options) + + commit(:authorize, post) + end + + def capture(money, authorization, options = {}) + post = {} + add_invoice(post, money, options) + add_reference(post, authorization) + + commit(:capture, post) + end + + def refund(money, authorization, options = {}) + post = {} + add_invoice(post, money, options) + add_reference(post, authorization) + + commit(:refund, post) + end + + def void(authorization, options = {}) + post = {} + add_reference(post, authorization) + + commit(:void, post) + end + + def scrub(transcript) + transcript. + gsub(%r(("processorId\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("merchantKey\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cardNumber\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("paymentCryptogram\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cvv\\?"\s*:\s*\\?)[^,]*)i, '\1[FILTERED]') + end + + private + + def add_address(post, creditcard, options) + if address = options[:billing_address] || options[:address] + post[:ownerName] = address[:name] + post[:ownerStreet] = address[:address1] + post[:ownerCity] = address[:city] + post[:ownerState] = address[:state] + post[:ownerZip] = address[:zip] + post[:ownerCountry] = address[:country] + end + end + + def add_invoice(post, money, options) + post[:orderId] = options[:order_id] + post[:transactionAmount] = amount(money) + end + + def add_payment(post, payment, options) + post[:cardNumber] = payment.number + post[:cardExpMonth] = payment.month + post[:cardExpYear] = format(payment.year, :two_digits) + post[:cvv] = payment.verification_value + post[:recurring] = options[:recurring] if options[:recurring] + post[:recurringStartDate] = options[:recurring_start_date] if options[:recurring_start_date] + post[:recurringEndDate] = options[:recurring_end_date] if options[:recurring_end_date] + + case payment + when NetworkTokenizationCreditCard + post[:walletType] = WALLET_TYPES[payment.source] + other_fields = post[:otherFields] = {} + other_fields[:paymentCryptogram] = payment.payment_cryptogram + other_fields[:eciIndicator] = payment.eci || '07' + when CreditCard + post[:cvv] = payment.verification_value + end + end + + def add_reference(post, authorization) + post[:refNumber] = authorization + end + + def commit(action, parameters) + response = parse(api_request(base_url + ACTIONS[action], post_data(parameters))) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + error_code: error_code_from(response), + test: test? + ) + end + + def base_url + test? ? self.test_url : self.live_url + end + + def api_request(url, data) + ssl_post(url, data, headers) + rescue ResponseError => e + e.response.body + end + + def parse(data) + JSON.parse data + end + + def headers + { 'Content-Type' => 'application/json' } + end + + def format_messages(messages) + return unless messages.present? + + messages.map { |message| message['message'] || message }.join('; ') + end + + def success_from(response) + response['isSuccess'] + end + + def message_from(response) + format_messages(response['errorMessages'] + response['validationFailures']) || response['data']['authResponse'] + end + + def error_code_from(response) + return 'isError' if response['isError'] + + return 'validationHasFailed' if response['validationHasFailed'] + end + + def authorization_from(response) + response.dig('data', 'referenceNumber') || '' + end + + def post_data(params) + params.merge({ processorId: @options[:processor_id], merchantKey: @options[:merchant_key] }).to_json + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/first_pay/first_pay_xml.rb b/lib/active_merchant/billing/gateways/first_pay/first_pay_xml.rb new file mode 100644 index 00000000000..fb111949920 --- /dev/null +++ b/lib/active_merchant/billing/gateways/first_pay/first_pay_xml.rb @@ -0,0 +1,183 @@ +require 'active_merchant/billing/gateways/first_pay/first_pay_common' +require 'nokogiri' + +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class FirstPayXmlGateway < Gateway + include FirstPayCommon + + self.live_url = 'https://secure.goemerchant.com/secure/gateway/xmlgateway.aspx' + + # Creates a new FirstPayXmlGateway + # + # The gateway requires two values for connection to be passed + # in the +options+ hash + # + # ==== Options + # + # * :gateway_id -- FirstPay's gateway_id (REQUIRED) + # * :transaction_center_id -- FirstPay's transaction_center_id or processorId (REQUIRED) + # Otherwise, perform transactions against the production server. + def initialize(options = {}) + requires!(options, :gateway_id, :transaction_center_id) + super + end + + def purchase(money, payment, options = {}) + post = {} + add_invoice(post, money, options) + add_payment(post, payment, options) + add_address(post, payment, options) + add_customer_data(post, options) + + commit('sale', post) + end + + def authorize(money, payment, options = {}) + post = {} + add_invoice(post, money, options) + add_payment(post, payment, options) + add_address(post, payment, options) + add_customer_data(post, options) + + commit('auth', post) + end + + def capture(money, authorization, options = {}) + post = {} + add_reference(post, 'settle', money, authorization) + commit('settle', post) + end + + def refund(money, authorization, options = {}) + post = {} + add_reference(post, 'credit', money, authorization) + commit('credit', post) + end + + def void(authorization, options = {}) + post = {} + add_reference(post, 'void', nil, authorization) + commit('void', post) + end + + def scrub(transcript) + transcript. + gsub(%r((gateway_id)[^<]*())i, '\1[FILTERED]\2'). + gsub(%r((card_number)[^<]*())i, '\1[FILTERED]\2'). + gsub(%r((cvv2)[^<]*())i, '\1[FILTERED]\2') + end + + private + + def add_authentication(post, options) + post[:transaction_center_id] = options[:transaction_center_id] + post[:gateway_id] = options[:gateway_id] + end + + def add_customer_data(post, options) + post[:owner_email] = options[:email] if options[:email] + post[:remote_ip_address] = options[:ip] if options[:ip] + post[:processor_id] = options[:processor_id] if options[:processor_id] + end + + def add_address(post, creditcard, options) + if address = options[:billing_address] || options[:address] + post[:owner_name] = address[:name] + post[:owner_street] = address[:address1] + post[:owner_street2] = address[:address2] if address[:address2] + post[:owner_city] = address[:city] + post[:owner_state] = address[:state] + post[:owner_zip] = address[:zip] + post[:owner_country] = address[:country] + post[:owner_phone] = address[:phone] if address[:phone] + end + end + + def add_invoice(post, money, options) + post[:order_id] = options[:order_id] + post[:total] = amount(money) + end + + def add_payment(post, payment, options) + post[:card_name] = payment.brand # Unclear if need to map to known names or open text field?? + post[:card_number] = payment.number + post[:card_exp] = expdate(payment) + post[:cvv2] = payment.verification_value + post[:recurring] = options[:recurring] if options[:recurring] + post[:recurring_start_date] = options[:recurring_start_date] if options[:recurring_start_date] + post[:recurring_end_date] = options[:recurring_end_date] if options[:recurring_end_date] + post[:recurring_type] = options[:recurring_type] if options[:recurring_type] + end + + def add_reference(post, action, money, authorization) + post[:"#{action}_amount1"] = amount(money) if money + post[:total_number_transactions] = 1 + post[:reference_number1] = authorization + end + + def parse(xml) + response = {} + + doc = Nokogiri::XML(xml) + doc.root&.xpath('//RESPONSE/FIELDS/FIELD')&.each do |field| + response[field['KEY']] = field.text + end + + response + end + + def commit(action, parameters) + response = parse(ssl_post(live_url, post_data(action, parameters))) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + error_code: error_code_from(response), + test: test? + ) + end + + def success_from(response) + ( + (response['status'] == '1') || + (response['status1'] == '1') + ) + end + + def message_from(response) + # Silly inconsistent gateway. Always make capitalized (but not all caps) + msg = (response['auth_response'] || response['response1']) + msg&.downcase&.capitalize + end + + def error_code_from(response) + response['error'] + end + + def authorization_from(response) + response['reference_number'] || response['reference_number1'] + end + + def post_data(action, parameters = {}) + parameters[:transaction_center_id] = @options[:transaction_center_id] + parameters[:gateway_id] = @options[:gateway_id] + + parameters[:operation_type] = action + + xml = Builder::XmlMarkup.new + xml.instruct! + xml.tag! 'TRANSACTION' do + xml.tag! 'FIELDS' do + parameters.each do |key, value| + xml.tag! 'FIELD', value, { 'KEY' => key } + end + end + end + xml.target! + end + end + end +end 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..caf783770ac 100644 --- a/lib/active_merchant/billing/gateways/firstdata_e4_v27.rb +++ b/lib/active_merchant/billing/gateways/firstdata_e4_v27.rb @@ -316,7 +316,7 @@ def add_address(xml, options) def strip_line_breaks(address) return unless address.is_a?(Hash) - Hash[address.map { |k, s| [k, s&.tr("\r\n", ' ')&.strip] }] + address.map { |k, s| [k, s&.tr("\r\n", ' ')&.strip] }.to_h end def add_invoice(xml, options) @@ -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/flex_charge.rb b/lib/active_merchant/billing/gateways/flex_charge.rb new file mode 100644 index 00000000000..b3ff85061b1 --- /dev/null +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -0,0 +1,296 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class FlexChargeGateway < Gateway + self.test_url = 'https://api-sandbox.flex-charge.com/v1/' + self.live_url = 'https://api.flex-charge.com/v1/' + + 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://www.flex-charge.com/' + self.display_name = 'FlexCharge' + + ENDPOINTS_MAPPING = { + authenticate: 'oauth2/token', + purchase: 'evaluate', + sync: 'outcome', + refund: 'orders/%s/refund', + store: 'tokenize', + inquire: 'orders/%s' + } + + SUCCESS_MESSAGES = %w(APPROVED CHALLENGE SUBMITTED SUCCESS PROCESSING).freeze + + def initialize(options = {}) + requires!(options, :app_key, :app_secret, :site_id, :mid) + super + end + + def purchase(money, credit_card, options = {}) + post = {} + address = options[:billing_address] || options[:address] + add_merchant_data(post, options) + add_base_data(post, options) + add_invoice(post, money, credit_card, options) + add_mit_data(post, options) + add_payment_method(post, credit_card, address, options) + add_address(post, credit_card, address) + add_customer_data(post, options) + add_three_ds(post, options) + + commit(:purchase, post) + end + + def refund(money, authorization, options = {}) + commit(:refund, { amountToRefund: (money.to_f / 100).round(2) }, authorization) + end + + def store(credit_card, options = {}) + address = options[:billing_address] || options[:address] || {} + first_name, last_name = address_names(address[:name], credit_card) + + post = { + payment_method: { + credit_card: { + first_name: first_name, + last_name: last_name, + month: credit_card.month, + year: credit_card.year, + number: credit_card.number, + verification_value: credit_card.verification_value + }.compact + } + } + commit(:store, post) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Bearer )[a-zA-Z0-9._-]+)i, '\1[FILTERED]'). + gsub(%r(("AppKey\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("AppSecret\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("accessToken\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("mid\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("siteId\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("environment\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cardNumber\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("verification_value\\?":\\?")\d+), '\1[FILTERED]') + end + + def inquire(authorization, options = {}) + commit(:inquire, {}, authorization, :get) + end + + private + + def add_three_ds(post, options) + return unless three_d_secure = options[:three_d_secure] + + post[:threeDSecure] = { + threeDsVersion: three_d_secure[:version], + EcommerceIndicator: three_d_secure[:eci], + authenticationValue: three_d_secure[:cavv], + directoryServerTransactionId: three_d_secure[:ds_transaction_id], + xid: three_d_secure[:xid], + authenticationValueAlgorithm: three_d_secure[:cavv_algorithm], + directoryResponseStatus: three_d_secure[:directory_response_status], + authenticationResponseStatus: three_d_secure[:authentication_response_status], + enrolled: three_d_secure[:enrolled] + } + end + + def add_merchant_data(post, options) + post[:siteId] = @options[:site_id] + post[:mid] = @options[:mid] + end + + def add_base_data(post, options) + post[:isDeclined] = cast_bool(options[:is_declined]) + post[:orderId] = options[:order_id] + post[:idempotencyKey] = options[:idempotency_key] || options[:order_id] + end + + def add_mit_data(post, options) + return if options[:is_mit].nil? + + post[:isMIT] = cast_bool(options[:is_mit]) + post[:isRecurring] = cast_bool(options[:is_recurring]) + post[:expiryDateUtc] = options[:mit_expiry_date_utc] + end + + def add_customer_data(post, options) + post[:payer] = { email: options[:email] || 'NA', phone: phone_from(options) }.compact + end + + def add_address(post, payment, address) + first_name, last_name = address_names(address[:name], payment) + + post[:billingInformation] = { + firstName: first_name, + lastName: last_name, + country: address[:country], + phone: address[:phone], + countryCode: address[:country], + addressLine1: address[:address1], + state: address[:state], + city: address[:city], + zipCode: address[:zip] + }.compact + end + + def add_invoice(post, money, credit_card, options) + post[:transaction] = { + id: options[:order_id], + dynamicDescriptor: options[:description], + timestamp: Time.now.utc.iso8601, + timezoneUtcOffset: options[:timezone_utc_offset], + amount: money, + currency: (options[:currency] || currency(money)), + responseCode: options[:response_code], + responseCodeSource: options[:response_code_source] || '', + avsResultCode: options[:avs_result_code], + cvvResultCode: options[:cvv_result_code], + cavvResultCode: options[:cavv_result_code], + cardNotPresent: credit_card.is_a?(String) ? false : credit_card.verification_value.blank? + }.compact + end + + def add_payment_method(post, credit_card, address, options) + payment_method = case credit_card + when String + { Token: true, cardNumber: credit_card } + else + { + holderName: credit_card.name, + cardType: 'CREDIT', + cardBrand: credit_card.brand&.upcase, + cardCountry: address[:country], + expirationMonth: credit_card.month, + expirationYear: credit_card.year, + cardBinNumber: credit_card.number[0..5], + cardLast4Digits: credit_card.number[-4..-1], + cardNumber: credit_card.number, + Token: false + } + end + post[:paymentMethod] = payment_method.compact + end + + def address_names(address_name, payment_method) + split_names(address_name).tap do |names| + names[0] = payment_method&.first_name unless names[0].present? + names[1] = payment_method&.last_name unless names[1].present? + end + end + + def phone_from(options) + options[:phone] || options.dig(:billing_address, :phone_number) + end + + def access_token_valid? + @options[:access_token].present? && @options.fetch(:token_expires, 0) > DateTime.now.strftime('%Q').to_i + end + + def fetch_access_token + params = { AppKey: @options[:app_key], AppSecret: @options[:app_secret] } + response = parse(ssl_post(url(:authenticate), params.to_json, headers)) + + @options[:access_token] = response[:accessToken] + @options[:token_expires] = response[:expires] + @options[:new_credentials] = true + + Response.new( + response[:accessToken].present?, + message_from(response), + response, + test: test?, + error_code: response[:statusCode] + ) + rescue ResponseError => e + raise OAuthResponseError.new(e) + end + + def url(action, id = nil) + "#{test? ? test_url : live_url}#{ENDPOINTS_MAPPING[action] % id}" + end + + def headers + { 'Content-Type' => 'application/json' }.tap do |headers| + headers['Authorization'] = "Bearer #{@options[:access_token]}" if @options[:access_token] + end + end + + def parse(body) + JSON.parse(body).with_indifferent_access + rescue JSON::ParserError + { + errors: body, + status: 'Unable to parse JSON response' + }.with_indifferent_access + end + + def commit(action, post, authorization = nil, method = :post) + MultiResponse.run do |r| + r.process { fetch_access_token } unless access_token_valid? + r.process do + api_request(action, post, authorization, method).tap do |response| + response.params.merge!(@options.slice(:access_token, :token_expires)) if @options[:new_credentials] + end + end + end + end + + def api_request(action, post, authorization = nil, method = :post) + response = parse ssl_request(method, url(action, authorization), post.to_json, headers) + + Response.new( + success_from(action, response), + message_from(response), + response, + authorization: authorization_from(action, response), + test: test?, + error_code: error_code_from(action, response) + ) + rescue ResponseError => e + response = parse(e.response.body) + # if current access_token is invalid then clean it + if e.response.code == '401' + @options[:access_token] = '' + @options[:new_credentials] = true + end + Response.new(false, message_from(response), response, test: test?) + end + + def success_from(action, response) + case action + when :store then response.dig(:transaction, :payment_method, :token).present? + when :inquire then response[:id].present? && SUCCESS_MESSAGES.include?(response[:statusName]) + else + response[:success] && SUCCESS_MESSAGES.include?(response[:status]) + end + end + + def message_from(response) + response[:title] || response[:responseMessage] || response[:statusName] || response[:status] + end + + def authorization_from(action, response) + action == :store ? response.dig(:transaction, :payment_method, :token) : response[:orderId] + end + + def error_code_from(action, response) + (response[:statusName] || response[:status]) unless success_from(action, response) + end + + def cast_bool(value) + ![false, 0, '', '0', 'f', 'F', 'false', 'FALSE'].include?(value) + end + end + end +end 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 c1a44930a29..466b2be2de5 100644 --- a/lib/active_merchant/billing/gateways/global_collect.rb +++ b/lib/active_merchant/billing/gateways/global_collect.rb @@ -2,18 +2,22 @@ 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.display_name = 'Worldline (formerly GlobalCollect)' self.homepage_url = 'http://www.globalcollect.com/' 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.preproduction_url = 'https://api.preprod.connect.worldline-solutions.com' + self.live_url = 'https://api.connect.worldline-solutions.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' self.money_format = :cents - self.supported_cardtypes = %i[visa master american_express discover naranja cabal] + self.supported_cardtypes = %i[visa master american_express discover naranja cabal tuya] def initialize(options = {}) requires!(options, :merchant_id, :api_key_id, :secret_api_key) @@ -36,6 +40,7 @@ def authorize(money, payment, options = {}) add_creator_info(post, options) add_fraud_fields(post, options) add_external_cardholder_authentication_data(post, options) + add_threeds_exemption_data(post, options) commit(:post, :authorize, post, options: options) end @@ -97,8 +102,8 @@ def scrub(transcript) 'diners_club' => '132', 'cabal' => '135', 'naranja' => '136', - 'apple_pay': '302', - 'google_pay': '320' + apple_pay: '302', + google_pay: '320' } def add_order(post, money, options, capture: false) @@ -114,7 +119,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 @@ -134,6 +139,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) @@ -248,9 +254,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 @@ -262,7 +269,7 @@ def add_payment(post, payment, options) product_id = options[:payment_product_id] || BRAND_MAP[payment.brand] specifics_inputs = { 'paymentProductId' => product_id, - 'skipAuthentication' => 'true', # refers to 3DSecure + 'skipAuthentication' => options[:skip_authentication] || 'true', # refers to 3DSecure 'skipFraudService' => 'true', 'authorizationMode' => pre_authorization } @@ -270,7 +277,7 @@ def add_payment(post, payment, options) 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) + add_credit_card(post, payment, specifics_inputs, expirydate) end end @@ -286,31 +293,32 @@ def add_credit_card(post, payment, specifics_inputs, expirydate) 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] + specifics_inputs['paymentProductId'] = BRAND_MAP[payment.source] post['mobilePaymentMethodSpecificInput'] = specifics_inputs - add_decrypted_payment_data(post, payment, options, expirydate) + + if options[:use_encrypted_payment_data] + post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'] = payment.payment_data + else + add_decrypted_payment_data(post, payment, options, expirydate) + end 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 + data_type = payment.source == :apple_pay ? 'decrypted' : 'encrypted' + data = case payment.source + when :apple_pay + { + 'cardholderName' => payment.name, + 'cryptogram' => payment.payment_cryptogram, + 'eci' => payment.eci, + 'expiryDate' => expirydate, + 'dpan' => payment.number + } + when :google_pay + payment.payment_data + end + + post['mobilePaymentMethodSpecificInput']["#{data_type}PaymentData"] = data if data end def add_customer_data(post, options, payment = nil) @@ -321,8 +329,8 @@ def add_customer_data(post, options, payment = nil) post['order']['customer']['merchantCustomerId'] = options[:customer] if options[:customer] post['order']['customer']['companyInformation']['name'] = options[:company] if options[:company] post['order']['customer']['contactDetails']['emailAddress'] = options[:email] if options[:email] - if address = options[:billing_address] || options[:address] - post['order']['customer']['contactDetails']['phoneNumber'] = address[:phone] if address[:phone] + if address = options[:billing_address] || options[:address] && (address[:phone]) + post['order']['customer']['contactDetails']['phoneNumber'] = address[:phone] end end @@ -332,8 +340,8 @@ def add_refund_customer_data(post, options) 'countryCode' => address[:country] } post['customer']['contactDetails']['emailAddress'] = options[:email] if options[:email] - if address = options[:billing_address] || options[:address] - post['customer']['contactDetails']['phoneNumber'] = address[:phone] if address[:phone] + if address = options[:billing_address] || options[:address] && (address[:phone]) + post['customer']['contactDetails']['phoneNumber'] = address[:phone] end end end @@ -342,7 +350,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], @@ -352,7 +361,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], @@ -369,7 +379,6 @@ def add_address(post, creditcard, options) def add_fraud_fields(post, options) fraud_fields = {} fraud_fields.merge!(options[:fraud_fields]) if options[:fraud_fields] - fraud_fields[:customerIpAddress] = options[:ip] if options[:ip] post['fraudFields'] = fraud_fields unless fraud_fields.empty? end @@ -377,21 +386,33 @@ 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 + def add_threeds_exemption_data(post, options) + return unless options[:three_ds_exemption_type] + + post['cardPaymentMethodSpecificInput']['transactionChannel'] = 'MOTO' if options[:three_ds_exemption_type] == 'moto' + end + def add_number_of_installments(post, options) post['order']['additionalInput']['numberOfInstallments'] = options[:number_of_installments] if options[:number_of_installments] end @@ -402,17 +423,28 @@ 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 @@ -423,7 +455,7 @@ def uri(action, authorization) 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(method, action, post, authorization: nil, options: {}) @@ -461,7 +493,7 @@ def headers(method, action, post, authorization = nil, 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 @@ -474,13 +506,13 @@ def auth_digest(method, action, post, authorization = nil, 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.gmtime.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 @@ -490,8 +522,6 @@ 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/hi_pay.rb b/lib/active_merchant/billing/gateways/hi_pay.rb new file mode 100644 index 00000000000..30700ebc9f4 --- /dev/null +++ b/lib/active_merchant/billing/gateways/hi_pay.rb @@ -0,0 +1,286 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class HiPayGateway < Gateway + # to add more check => payment_product_list: https://developer.hipay.com/api-explorer/api-online-payments#/payments/generateHostedPaymentPage + PAYMENT_PRODUCT = { + 'visa' => 'visa', + 'master' => 'mastercard' + } + + DEVICE_CHANEL = { + app: 1, + browser: 2, + three_ds_requestor_initiaded: 3 + } + + self.test_url = 'https://stage-secure-gateway.hipay-tpp.com/rest' + self.live_url = 'https://secure-gateway.hipay-tpp.com/rest' + + self.supported_countries = %w[FR] + self.default_currency = 'EUR' + self.money_format = :dollars + self.supported_cardtypes = %i[visa master american_express] + + self.homepage_url = 'https://hipay.com/' + self.display_name = 'HiPay' + + def initialize(options = {}) + requires!(options, :username, :password) + @username = options[:username] + @password = options[:password] + super + end + + def purchase(money, payment_method, options = {}) + authorize(money, payment_method, options.merge({ operation: 'Sale' })) + end + + def authorize(money, payment_method, options = {}) + MultiResponse.run do |r| + if payment_method.is_a?(CreditCard) + response = r.process { tokenize(payment_method, options) } + card_token = response.params['token'] + elsif payment_method.is_a?(String) + _transaction_ref, card_token, payment_product = payment_method.split('|') if payment_method.split('|').size == 3 + card_token, payment_product = payment_method.split('|') if payment_method.split('|').size == 2 + end + + payment_product = payment_method.is_a?(CreditCard) ? PAYMENT_PRODUCT[payment_method.brand] : PAYMENT_PRODUCT[payment_product&.downcase] + + post = { + payment_product: payment_product, + operation: options[:operation] || 'Authorization', + cardtoken: card_token + } + add_address(post, options) + add_product_data(post, options) + add_invoice(post, money, options) + add_3ds(post, options) + r.process { commit('order', post) } + end + end + + def capture(money, authorization, options) + reference_operation(money, authorization, options.merge({ operation: 'capture' })) + end + + def store(payment_method, options = {}) + tokenize(payment_method, options.merge({ multiuse: '1' })) + end + + def unstore(authorization, options = {}) + _transaction_ref, card_token, _payment_product = authorization.split('|') if authorization.split('|').size == 3 + card_token, _payment_product = authorization.split('|') if authorization.split('|').size == 2 + commit('unstore', { card_token: card_token }, options, :delete) + end + + def refund(money, authorization, options) + reference_operation(money, authorization, options.merge({ operation: 'refund' })) + end + + def void(authorization, options) + reference_operation(nil, authorization, options.merge({ operation: 'cancel' })) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Basic )[\w =]+), '\1[FILTERED]'). + gsub(%r((card_number=)\w+), '\1[FILTERED]\2'). + gsub(%r((cvc=)\w+), '\1[FILTERED]\2') + end + + private + + def reference_operation(money, authorization, options) + post = {} + post[:operation] = options[:operation] + post[:currency] = (options[:currency] || currency(money)) + post[:amount] = amount(money) if options[:operation] == 'refund' || options[:operation] == 'capture' + commit(options[:operation], post, { transaction_reference: authorization.split('|').first }) + end + + def add_product_data(post, options) + post[:orderid] = options[:order_id] if options[:order_id] + post[:description] = options[:description] + end + + def add_invoice(post, money, options) + post[:currency] = (options[:currency] || currency(money)) + post[:amount] = amount(money) + end + + def add_credit_card(post, credit_card) + post[:card_number] = credit_card.number + post[:card_expiry_month] = credit_card.month + post[:card_expiry_year] = credit_card.year + post[:card_holder] = credit_card.name + post[:cvc] = credit_card.verification_value + end + + def add_address(post, options) + return unless billing_address = options[:billing_address] + + post[:streetaddress] = billing_address[:address1] if billing_address[:address1] + post[:streetaddress2] = billing_address[:address2] if billing_address[:address2] + post[:city] = billing_address[:city] if billing_address[:city] + post[:recipient_info] = billing_address[:company] if billing_address[:company] + post[:state] = billing_address[:state] if billing_address[:state] + post[:country] = billing_address[:country] if billing_address[:country] + post[:zipcode] = billing_address[:zip] if billing_address[:zip] + post[:country] = billing_address[:country] if billing_address[:country] + post[:phone] = billing_address[:phone] if billing_address[:phone] + end + + def tokenize(payment_method, options = {}) + post = {} + add_credit_card(post, payment_method) + post[:multi_use] = options[:multiuse] ? '1' : '0' + post[:generate_request_id] = '0' + commit('store', post, options) + end + + def add_3ds(post, options) + return unless options.has_key?(:execute_threed) + + browser_info_3ds = options[:three_ds_2][:browser_info] + + browser_info_hash = { + java_enabled: browser_info_3ds[:java], + javascript_enabled: (browser_info_3ds[:javascript] || false), + ipaddr: options[:ip], + http_accept: '*\\/*', + http_user_agent: browser_info_3ds[:user_agent], + language: browser_info_3ds[:language], + color_depth: browser_info_3ds[:depth], + screen_height: browser_info_3ds[:height], + screen_width: browser_info_3ds[:width], + timezone: browser_info_3ds[:timezone] + } + + browser_info_hash['device_fingerprint'] = options[:device_fingerprint] if options[:device_fingerprint] + post[:browser_info] = browser_info_hash.to_json + post.to_json + + post[:accept_url] = options[:accept_url] if options[:accept_url] + post[:decline_url] = options[:decline_url] if options[:decline_url] + post[:pending_url] = options[:pending_url] if options[:pending_url] + post[:exception_url] = options[:exception_url] if options[:exception_url] + post[:cancel_url] = options[:cancel_url] if options[:cancel_url] + post[:notify_url] = browser_info_3ds[:notification_url] if browser_info_3ds[:notification_url] + post[:authentication_indicator] = DEVICE_CHANEL[options[:three_ds_2][:channel]] || 0 + end + + def parse(body) + return {} if body.blank? + + JSON.parse(body) + end + + def commit(action, post, options = {}, method = :post) + raw_response = begin + ssl_request(method, url(action, options), post_data(post), request_headers) + rescue ResponseError => e + e.response.body + end + + response = parse(raw_response) + + 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 error_code_from(action, response) + (response['code'] || response.dig('reason', 'code')).to_s unless success_from(action, response) + end + + def success_from(action, response) + case action + when 'order' + response['state'] == 'completed' || (response['state'] == 'forwarding' && response['status'] == '140') + when 'capture' + response['status'] == '118' && response['message'] == 'Captured' + when 'refund' + response['status'] == '124' && response['message'] == 'Refund Requested' + when 'cancel' + response['status'] == '175' && response['message'] == 'Authorization Cancellation requested' + when 'store' + response.include? 'token' + when 'unstore' + response['code'] == '204' + else + false + end + end + + def message_from(action, response) + response['message'] + end + + def authorization_from(action, response) + authorization_string(response['transactionReference'], response['token'], response['brand']) + end + + def authorization_string(*args) + args.flatten.compact.reject(&:empty?).join('|') + end + + def post_data(params) + params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') + end + + def url(action, options = {}) + case action + when 'store' + "#{token_url}/create" + when 'unstore' + token_url + when 'capture', 'refund', 'cancel' + endpoint = "maintenance/transaction/#{options[:transaction_reference]}" + base_url(endpoint) + else + base_url(action) + end + end + + def base_url(endpoint) + "#{test? ? test_url : live_url}/v1/#{endpoint}" + end + + def token_url + "https://#{'stage-' if test?}secure2-vault.hipay-tpp.com/rest/v2/token" + end + + def basic_auth + Base64.strict_encode64("#{@username}:#{@password}") + end + + def request_headers + { + 'Accept' => 'application/json', + 'Content-Type' => 'application/x-www-form-urlencoded', + 'Authorization' => "Basic #{basic_auth}" + } + 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 || { code: response.code }.to_json + else + raise ResponseError.new(response) + end + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/hps.rb b/lib/active_merchant/billing/gateways/hps.rb index 5bd92b80e02..a4a6370b992 100644 --- a/lib/active_merchant/billing/gateways/hps.rb +++ b/lib/active_merchant/billing/gateways/hps.rb @@ -330,7 +330,7 @@ def build_request(action) } do xml.SOAP :Body do xml.hps :PosRequest do - xml.hps 'Ver1.0'.to_sym do + xml.hps :"Ver1.0" do xml.hps :Header do xml.hps :SecretAPIKey, @options[:secret_api_key] xml.hps :DeveloperID, @options[:developer_id] if @options[:developer_id] 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..0347d97ec25 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) @@ -196,8 +199,7 @@ def post_data(action, parameters = {}) post[:password] = @options[:password] post[:type] = action if action - request = post.merge(parameters).map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') - request + post.merge(parameters).map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end def determine_funding_source(source) diff --git a/lib/active_merchant/billing/gateways/instapay.rb b/lib/active_merchant/billing/gateways/instapay.rb index 4ca11852dd8..76e9de5d556 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 = {}) @@ -151,8 +155,7 @@ def post_data(action, parameters = {}) post[:acctid] = @options[:login] post[:merchantpin] = @options[:password] if @options[:password] post[:action] = action - request = post.merge(parameters).collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') - request + post.merge(parameters).collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end end end diff --git a/lib/active_merchant/billing/gateways/ipg.rb b/lib/active_merchant/billing/gateways/ipg.rb index 46052cfd1e7..10b3bfbaaed 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,7 +18,7 @@ class IpgGateway < Gateway ACTION_REQUEST_ITEMS = %w(vault unstore) def initialize(options = {}) - requires!(options, :store_id, :user_id, :password, :pem, :pem_password) + requires!(options, :user_id, :password, :pem, :pem_password) @credentials = options @hosted_data_id = nil super @@ -86,8 +86,7 @@ def scrub(transcript) transcript. gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). gsub(%r(().+()), '\1[FILTERED]\2'). - gsub(%r(().+()), '\1[FILTERED]\2'). - gsub(%r(().+()), '\1[FILTERED]\2') + gsub(%r(().+()), '\1[FILTERED]\2') end private @@ -273,7 +272,7 @@ def add_payment(xml, money, payment, options) xml.tag!('v1:SubTotal', options[:sub_total]) if options[:sub_total] xml.tag!('v1:ValueAddedTax', options[:value_added_tax]) if options[:value_added_tax] xml.tag!('v1:DeliveryAmount', options[:delivery_amount]) if options[:delivery_amount] - xml.tag!('v1:ChargeTotal', money) + xml.tag!('v1:ChargeTotal', amount(money)) xml.tag!('v1:Currency', CURRENCY_CODES[options[:currency]]) xml.tag!('v1:numberOfInstallments', options[:number_of_installments]) if options[:number_of_installments] end @@ -317,7 +316,10 @@ def build_header end def encoded_credentials - Base64.encode64("WS#{@credentials[:store_id]}._.#{@credentials[:user_id]}:#{@credentials[:password]}").delete("\n") + # We remove 'WS' and add it back on the next line because the ipg docs are a little confusing. + # Some merchants will likely add it to their user_id and others won't. + user_id = @credentials[:user_id].sub(/^WS/, '') + Base64.encode64("WS#{user_id}:#{@credentials[:password]}").delete("\n") end def envelope_namespaces @@ -344,6 +346,8 @@ def ipg_action_namespaces end def override_store_id(options) + raise ArgumentError, 'store_id must be provieded' if @credentials[:store_id].blank? && options[:store_id].blank? + @credentials[:store_id] = options[:store_id] if options[:store_id].present? end @@ -396,7 +400,7 @@ def parse_element(reply, node) 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 87993b01baf..c2cb8aa141a 100644 --- a/lib/active_merchant/billing/gateways/iveri.rb +++ b/lib/active_merchant/billing/gateways/iveri.rb @@ -218,10 +218,10 @@ def parse_element(parsed, node) end end - if !node.elements.empty? - node.elements.each { |e| parse_element(parsed, e) } - else + if node.elements.empty? parsed[underscore(node.name)] = node.text + else + node.elements.each { |e| parse_element(parsed, e) } 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 6125ea41c35..6e10b0876a2 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 @@ -48,8 +48,9 @@ def refund(amount, authorization, options = {}) post = {} post[:ticketNumber] = authorization add_full_response(post, options) + add_invoice(action, post, amount, options) - commit(action, post) + commit(action, post, options) end def void(authorization, options = {}) @@ -83,11 +84,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 +99,15 @@ 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) + add_product_details(post, options) commit(action, post) end - def preauthorize(amount, authorization, options) + def preauthorize(amount, authorization, options, payment_method = {}) action = 'preAuthorization' post = {} @@ -108,6 +115,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 +137,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 @@ -177,13 +187,81 @@ def add_contact_details(post, contact_details_options) end def add_full_response(post, options) - post[:fullResponse] = options[:full_response].to_s.casecmp('true').zero? if options[:full_response] + # this is the only currently accepted value for this field, previously it was 'true' + post[:fullResponse] = 'v2' unless options[:full_response] == 'false' || options[:full_response].blank? end 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_product_details(post, options) + return unless options[:product_details] + + product_items_array = [] + options[:product_details].each do |item| + product_items_obj = {} + + product_items_obj[:id] = item[:id] if item[:id] + product_items_obj[:title] = item[:title] if item[:title] + product_items_obj[:price] = item[:price].to_i if item[:price] + product_items_obj[:sku] = item[:sku] if item[:sku] + product_items_obj[:quantity] = item[:quantity].to_i if item[:quantity] + + product_items_array << product_items_obj + end + + product_items = { + product: product_items_array + } + + post[:productDetails] = product_items + 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', @@ -193,10 +271,10 @@ def add_metadata(post, options) 'capture' => 'capture' } - def commit(action, params) + def commit(action, params, options = {}) response = begin - parse(ssl_invoke(action, params)) + parse(ssl_invoke(action, params, options)) rescue ResponseError => e parse(e.response.body) end @@ -213,9 +291,11 @@ def commit(action, params) ) end - def ssl_invoke(action, params) + def ssl_invoke(action, params, options) if %w[void refund].include?(action) - ssl_request(:delete, url(action, params), nil, headers(action)) + # removes ticketNumber from request for partial refunds because gateway will reject if included in request body + data = options[:partial_refund] == true ? post_data(params.except(:ticketNumber)) : nil + ssl_request(:delete, url(action, params), data, headers(action)) else ssl_post(url(action, params), post_data(params), headers(action)) end 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 be6e34e9562..49b4eed8e44 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' @@ -110,14 +111,15 @@ def add_line_item_information_for_level_three_visa(doc, payment_method, level_3_ doc.lineItemData do level_3_data[:line_items].each do |line_item| doc.itemSequenceNumber(line_item[:item_sequence_number]) if line_item[:item_sequence_number] - doc.commodityCode(line_item[:commodity_code]) if line_item[:commodity_code] doc.itemDescription(line_item[:item_description]) if line_item[:item_description] doc.productCode(line_item[:product_code]) if line_item[:product_code] doc.quantity(line_item[:quantity]) if line_item[:quantity] doc.unitOfMeasure(line_item[:unit_of_measure]) if line_item[:unit_of_measure] doc.taxAmount(line_item[:tax_amount]) if line_item[:tax_amount] - doc.itemDiscountAmount(line_item[:discount_per_line_item]) unless line_item[:discount_per_line_item] < 0 - doc.unitCost(line_item[:unit_cost]) unless line_item[:unit_cost] < 0 + doc.lineItemTotal(line_item[:line_item_total]) if line_item[:line_item_total] + doc.itemDiscountAmount(line_item[:discount_per_line_item].to_i) unless line_item[:discount_per_line_item].to_i < 0 + doc.commodityCode(line_item[:commodity_code]) if line_item[:commodity_code] + doc.unitCost(line_item[:unit_cost].to_i) unless line_item[:unit_cost].to_i < 0 doc.detailTax do doc.taxIncludedInTotal(line_item[:tax_included_in_total]) if line_item[:tax_included_in_total] doc.taxAmount(line_item[:tax_amount]) if line_item[:tax_amount] @@ -309,7 +311,15 @@ def add_authentication(doc) def add_auth_purchase_params(doc, money, payment_method, options) doc.orderId(truncate(options[:order_id], 24)) doc.amount(money) - add_order_source(doc, payment_method, options) + + if options.dig(:stored_credential, :initial_transaction) == false + # orderSource needs to be added at the top of doc and + # processingType near the end + source_for_subsequent_stored_credential_txns(doc, options) + else + add_order_source(doc, payment_method, options) + end + add_billing_address(doc, payment_method, options) add_shipping_address(doc, payment_method, options) add_payment_method(doc, payment_method, options) @@ -381,8 +391,9 @@ def add_payment_method(doc, payment_method, options) doc.track(payment_method.track_data) end elsif check?(payment_method) + account_type = payment_method.account_type || payment_method.account_holder_type doc.echeck do - doc.accType(payment_method.account_type.capitalize) + doc.accType(account_type&.capitalize) doc.accNum(payment_method.account_number) doc.routingNum(payment_method.routing_number) doc.checkNum(payment_method.number) if payment_method.number @@ -408,16 +419,17 @@ def add_payment_method(doc, payment_method, options) end def add_stored_credential_params(doc, options = {}) - return unless options[:stored_credential] + return unless stored_credential = options[:stored_credential] - if options[:stored_credential][:initial_transaction] - add_stored_credential_params_initial(doc, options) + if stored_credential[:initial_transaction] + add_stored_credential_for_initial_txn(doc, options) else - add_stored_credential_params_used(doc, options) + doc.processingType("#{stored_credential[:initiator]}InitiatedCOF") if stored_credential[:reason_type] == 'unscheduled' + doc.originalNetworkTransactionId(stored_credential[:network_transaction_id]) if stored_credential[:initiator] == 'merchant' end end - def add_stored_credential_params_initial(doc, options) + def add_stored_credential_for_initial_txn(doc, options) case options[:stored_credential][:reason_type] when 'unscheduled' doc.processingType('initialCOF') @@ -428,15 +440,15 @@ def add_stored_credential_params_initial(doc, options) end end - def add_stored_credential_params_used(doc, options) - if options[:stored_credential][:reason_type] == 'unscheduled' - if options[:stored_credential][:initiator] == 'merchant' - doc.processingType('merchantInitiatedCOF') - else - doc.processingType('cardholderInitiatedCOF') - end + def source_for_subsequent_stored_credential_txns(doc, options) + case options[:stored_credential][:reason_type] + when 'unscheduled' + doc.orderSource('ecommerce') + when 'installment' + doc.orderSource('installment') + when 'recurring' + doc.orderSource('recurring') end - doc.originalNetworkTransactionId(options[:stored_credential][:network_transaction_id]) end def add_billing_address(doc, payment_method, options) @@ -478,8 +490,7 @@ def add_address(doc, address) end def add_order_source(doc, payment_method, options) - order_source = order_source(options) - if order_source + if order_source = options[:order_source] doc.orderSource(order_source) elsif payment_method.is_a?(NetworkTokenizationCreditCard) && payment_method.source == :apple_pay doc.orderSource('applepay') @@ -492,31 +503,6 @@ def add_order_source(doc, payment_method, options) end end - def order_source(options = {}) - return options[:order_source] unless options[:stored_credential] - - order_source = nil - - case options[:stored_credential][:reason_type] - when 'unscheduled' - if options[:stored_credential][:initiator] == 'merchant' - # For merchant-initiated, we should always set order source to - # 'ecommerce' - order_source = 'ecommerce' - else - # For cardholder-initiated, we rely on #add_order_source's - # default logic to set orderSource appropriately - order_source = options[:order_source] - end - when 'installment' - order_source = 'installment' - when 'recurring' - order_source = 'recurring' - end - - order_source - end - def add_pos(doc, payment_method) return unless payment_method.respond_to?(:track_data) && payment_method.track_data.present? @@ -571,15 +557,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 141 142).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 @@ -606,16 +603,15 @@ def root_attributes } end - def build_xml_request + def build_xml_request(&block) builder = Nokogiri::XML::Builder.new - builder.__send__('litleOnlineRequest', root_attributes) do |doc| - yield(doc) - end + builder.__send__('litleOnlineRequest', root_attributes, &block) builder.doc.root.to_xml end 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/maxipago.rb b/lib/active_merchant/billing/gateways/maxipago.rb index c22ceaeaf01..57c9bca9763 100644 --- a/lib/active_merchant/billing/gateways/maxipago.rb +++ b/lib/active_merchant/billing/gateways/maxipago.rb @@ -79,8 +79,8 @@ def scrub(transcript) private - def commit(action) - request = build_xml_request(action) { |doc| yield(doc) } + def commit(action, &block) + request = build_xml_request(action, &block) response = parse(ssl_post(url, request, 'Content-Type' => 'text/xml')) Response.new( diff --git a/lib/active_merchant/billing/gateways/merchant_e_solutions.rb b/lib/active_merchant/billing/gateways/merchant_e_solutions.rb index 6c6b2bc85c8..86cdb5c8bc6 100644 --- a/lib/active_merchant/billing/gateways/merchant_e_solutions.rb +++ b/lib/active_merchant/billing/gateways/merchant_e_solutions.rb @@ -180,7 +180,7 @@ def parse(body) def commit(action, money, parameters) url = test? ? self.test_url : self.live_url - parameters[:transaction_amount] = amount(money) if money unless action == 'V' + parameters[:transaction_amount] = amount(money) if !(action == 'V') && money response = begin @@ -189,11 +189,15 @@ def commit(action, money, parameters) { 'error_code' => '404', 'auth_response_text' => e.to_s } end - Response.new(success_from(response), message_from(response), response, + 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) @@ -220,8 +224,7 @@ def post_data(action, parameters = {}) post[:profile_key] = @options[:password] post[:transaction_type] = action if action - request = post.merge(parameters).map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') - request + post.merge(parameters).map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end end end 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/merchant_warrior.rb b/lib/active_merchant/billing/gateways/merchant_warrior.rb index 337b18be643..18be04361f1 100644 --- a/lib/active_merchant/billing/gateways/merchant_warrior.rb +++ b/lib/active_merchant/billing/gateways/merchant_warrior.rb @@ -32,6 +32,7 @@ def authorize(money, payment_method, options = {}) add_payment_method(post, payment_method) add_recurring_flag(post, options) add_soft_descriptors(post, options) + add_three_ds(post, options) commit('processAuth', post) end @@ -43,6 +44,7 @@ def purchase(money, payment_method, options = {}) add_payment_method(post, payment_method) add_recurring_flag(post, options) add_soft_descriptors(post, options) + add_three_ds(post, options) commit('processCard', post) end @@ -184,6 +186,18 @@ def void_verification_hash(transaction_id) ) end + def add_three_ds(post, options) + return unless three_d_secure = options[:three_d_secure] + + post.merge!({ + threeDSEci: three_d_secure[:eci], + threeDSXid: three_d_secure[:xid] || three_d_secure[:ds_transaction_id], + threeDSCavv: three_d_secure[:cavv], + threeDSStatus: three_d_secure[:authentication_response_status], + threeDSV2Version: three_d_secure[:version] + }.compact) + end + def parse(body) xml = REXML::Document.new(body) 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..cb3ea1ad26a 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) @@ -194,7 +198,7 @@ def fraud_review?(response) def parse(body) fields = split(body) - results = { + { response_code: fields[RESPONSE_CODE].to_i, response_reason_code: fields[RESPONSE_REASON_CODE], response_reason_text: fields[RESPONSE_REASON_TEXT], @@ -202,7 +206,6 @@ def parse(body) transaction_id: fields[TRANSACTION_ID], card_code: fields[CARD_CODE_RESPONSE_CODE] } - results end def post_data(action, parameters = {}) @@ -218,8 +221,7 @@ def post_data(action, parameters = {}) post[:encap_char] = '$' post[:solution_ID] = application_id if application_id - request = post.merge(parameters).collect { |key, value| "x_#{key}=#{CGI.escape(value.to_s)}" }.join('&') - request + post.merge(parameters).collect { |key, value| "x_#{key}=#{CGI.escape(value.to_s)}" }.join('&') end def add_invoice(post, options) 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/migs/migs_codes.rb b/lib/active_merchant/billing/gateways/migs/migs_codes.rb index 32929ed8abe..dff303a5b81 100644 --- a/lib/active_merchant/billing/gateways/migs/migs_codes.rb +++ b/lib/active_merchant/billing/gateways/migs/migs_codes.rb @@ -71,6 +71,7 @@ module MigsCodes class CreditCardType attr_accessor :am_code, :migs_code, :migs_long_code, :name + def initialize(am_code, migs_code, migs_long_code, name) @am_code = am_code @migs_code = migs_code diff --git a/lib/active_merchant/billing/gateways/mit.rb b/lib/active_merchant/billing/gateways/mit.rb index 74b7bf6beab..2e2d6a97013 100644 --- a/lib/active_merchant/billing/gateways/mit.rb +++ b/lib/active_merchant/billing/gateways/mit.rb @@ -7,6 +7,7 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: class MitGateway < Gateway self.live_url = 'https://wpy.mitec.com.mx/ModuloUtilWS/activeCDP.htm' + self.test_url = 'https://scqa.mitec.com.mx/ModuloUtilWS/activeCDP.htm' self.supported_countries = ['MX'] self.default_currency = 'MXN' @@ -41,7 +42,7 @@ def decrypt(val, keyinhex) # original message full_data = unpacked[0].bytes.slice(16, unpacked[0].bytes.length) # Creates the engine - engine = OpenSSL::Cipher::AES128.new(:CBC) + engine = OpenSSL::Cipher.new('aes-128-cbc') # Set engine as decrypt mode engine.decrypt # Converts the key from hex to bytes @@ -54,7 +55,7 @@ def decrypt(val, keyinhex) def encrypt(val, keyinhex) # Creates the engine motor - engine = OpenSSL::Cipher::AES128.new(:CBC) + engine = OpenSSL::Cipher.new('aes-128-cbc') # Set engine as encrypt mode engine.encrypt # Converts the key from hex to bytes @@ -93,8 +94,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 +114,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 +135,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 +143,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 +168,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 +180,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 +190,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 @@ -218,10 +221,12 @@ def add_payment(post, payment) post[:name_client] = [payment.first_name, payment.last_name].join(' ') end + def url + test? ? test_url : live_url + 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(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 c7e1a5b9b5a..f9bb672fcd6 100755 --- a/lib/active_merchant/billing/gateways/monei.rb +++ b/lib/active_merchant/billing/gateways/monei.rb @@ -337,7 +337,7 @@ def commit(request, action, options) endpoint = translate_action_endpoint(action, options) headers = { 'Content-Type': 'application/json;charset=UTF-8', - 'Authorization': @options[:api_key], + Authorization: @options[:api_key], 'User-Agent': 'MONEI/Shopify/0.1.0' } diff --git a/lib/active_merchant/billing/gateways/moneris.rb b/lib/active_merchant/billing/gateways/moneris.rb index 1491e1315fa..2df428bb68c 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,11 +49,12 @@ 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) add_stored_credential(post, options) + add_cust_id(post, options) action = if post[:cavv] || options[:three_d_secure] 'cavv_preauth' elsif post[:data_key].blank? @@ -71,11 +74,12 @@ 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) add_stored_credential(post, options) + add_cust_id(post, options) action = if post[:cavv] || options[:three_d_secure] 'cavv_purchase' elsif post[:data_key].blank? @@ -246,6 +250,10 @@ def add_cof(post, options) post[:payment_information] = options[:payment_information] if options[:payment_information] end + def add_cust_id(post, options) + post[:cust_id] = options[:cust_id] if options[:cust_id] + end + def add_stored_credential(post, options) add_cof(post, options) # if any of :issuer_id, :payment_information, or :payment_indicator is not passed, @@ -438,6 +446,18 @@ def wallet_indicator(token_source) }[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 + + def truncate_order_id(order_id = nil) + order_id.present? ? order_id[0, 100] : SecureRandom.alphanumeric(100) + end + def message_from(message) return 'Unspecified error' if message.blank? @@ -453,8 +473,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/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/netbanx.rb b/lib/active_merchant/billing/gateways/netbanx.rb index b50053583df..fd0a493a30e 100644 --- a/lib/active_merchant/billing/gateways/netbanx.rb +++ b/lib/active_merchant/billing/gateways/netbanx.rb @@ -233,7 +233,7 @@ def map_address(address) end def map_3ds(three_d_secure_options) - mapped = { + { eci: three_d_secure_options[:eci], cavv: three_d_secure_options[:cavv], xid: three_d_secure_options[:xid], @@ -241,8 +241,6 @@ def map_3ds(three_d_secure_options) threeDSecureVersion: three_d_secure_options[:version], directoryServerTransactionId: three_d_secure_options[:ds_transaction_id] } - - mapped end def parse(body) 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..bb53c39eedf 100644 --- a/lib/active_merchant/billing/gateways/nmi.rb +++ b/lib/active_merchant/billing/gateways/nmi.rb @@ -8,7 +8,7 @@ class NmiGateway < Gateway self.test_url = self.live_url = 'https://secure.networkmerchants.com/api/transact.php' self.default_currency = 'USD' self.money_format = :dollars - self.supported_countries = ['US'] + self.supported_countries = %w[US CA] self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'http://nmi.com/' self.display_name = 'NMI' @@ -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) @@ -211,7 +212,8 @@ def add_stored_credential(post, options) else post[:stored_credential_indicator] = 'used' # should only send :initial_transaction_id if it is a MIT - post[:initial_transaction_id] = stored_credential[:network_transaction_id] if post[:initiated_by] == 'merchant' + ntid = options[:network_transaction_id] || stored_credential[:network_transaction_id] + post[:initial_transaction_id] = ntid if post[:initiated_by] == 'merchant' end end @@ -232,6 +234,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 +245,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]) @@ -329,8 +335,7 @@ def split_authorization(authorization) end def headers - headers = { 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8' } - headers + { 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8' } end def post_data(action, params) @@ -342,7 +347,7 @@ def url end def parse(body) - Hash[CGI::parse(body).map { |k, v| [k.intern, v.first] }] + CGI::parse(body).map { |k, v| [k.intern, v.first] }.to_h end def success_from(response) diff --git a/lib/active_merchant/billing/gateways/ogone.rb b/lib/active_merchant/billing/gateways/ogone.rb index 8c0fc958222..03b969d8df8 100644 --- a/lib/active_merchant/billing/gateways/ogone.rb +++ b/lib/active_merchant/billing/gateways/ogone.rb @@ -263,7 +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') @@ -460,7 +460,7 @@ def calculate_signature(signed_parameters, algorithm, secret) raise "Unknown signature algorithm #{algorithm}" end - filtered_params = signed_parameters.compact + filtered_params = signed_parameters.reject { |_k, v| v.nil? || v == '' } sha_encryptor.hexdigest( filtered_params.sort_by { |k, _v| k.upcase }.map { |k, v| "#{k.upcase}=#{v}#{secret}" }.join('') ).upcase @@ -494,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 7b648f75e94..e655a5b599a 100644 --- a/lib/active_merchant/billing/gateways/openpay.rb +++ b/lib/active_merchant/billing/gateways/openpay.rb @@ -201,11 +201,13 @@ 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 = {}) 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 c43a9f51b2e..872fda576c7 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 = '9.0' + API_VERSION = '9.5' POST_HEADERS = { 'MIME-Version' => '1.1', @@ -193,6 +193,10 @@ class OrbitalGateway < Gateway 'checking' => 'C' } + # safetech token flags + GET_TOKEN = 'GT' + USE_TOKEN = 'UT' + def initialize(options = {}) requires!(options, :merchant_id) requires!(options, :login, :password) unless options[:ip_authentication] @@ -253,6 +257,11 @@ def credit(money, payment_method, options = {}) commit(order, :refund, options[:retry_logic], options[:trace_number]) end + # Orbital save a payment method if the TokenTxnType is 'GT', that's why we use this as the default value for store + def store(creditcard, options = {}) + authorize(0, creditcard, options.merge({ token_txn_type: GET_TOKEN })) + end + def void(authorization, options = {}, deprecated = {}) if !options.kind_of?(Hash) ActiveMerchant.deprecated('Calling the void method with an amount parameter is deprecated and will be removed in a future version.') @@ -487,6 +496,35 @@ def add_line_items(xml, options = {}) end end + def add_level2_card_and_more_tax(xml, options = {}) + if (level2 = options[:level_2_data]) + xml.tag! :PCardRequestorName, byte_limit(level2[:requestor_name], 38) if level2[:requestor_name] + xml.tag! :PCardLocalTaxRate, byte_limit(level2[:local_tax_rate], 5) if level2[:local_tax_rate] + # Canadian Merchants Only + xml.tag! :PCardNationalTax, byte_limit(level2[:national_tax], 12) if level2[:national_tax] + xml.tag! :PCardPstTaxRegNumber, byte_limit(level2[:pst_tax_reg_number], 15) if level2[:pst_tax_reg_number] + xml.tag! :PCardCustomerVatRegNumber, byte_limit(level2[:customer_vat_reg_number], 13) if level2[:customer_vat_reg_number] + # Canadian Merchants Only + xml.tag! :PCardMerchantVatRegNumber, byte_limit(level2[:merchant_vat_reg_number], 20) if level2[:merchant_vat_reg_number] + xml.tag! :PCardTotalTaxAmount, byte_limit(level2[:total_tax_amount], 12) if level2[:total_tax_amount] + end + end + + def add_card_commodity_code(xml, options = {}) + if (level2 = options[:level_2_data]) && (level2[:commodity_code]) + xml.tag! :PCardCommodityCode, byte_limit(level2[:commodity_code], 4) + end + end + + def add_level3_vat_fields(xml, options = {}) + if (level3 = options[:level_3_data]) + xml.tag! :PC3InvoiceDiscTreatment, byte_limit(level3[:invoice_discount_treatment], 1) if level3[:invoice_discount_treatment] + xml.tag! :PC3TaxTreatment, byte_limit(level3[:tax_treatment], 1) if level3[:tax_treatment] + xml.tag! :PC3UniqueVATInvoiceRefNum, byte_limit(level3[:unique_vat_invoice_ref], 15) if level3[:unique_vat_invoice_ref] + xml.tag! :PC3ShipVATRate, byte_limit(level3[:ship_vat_rate], 4) if level3[:ship_vat_rate] + end + end + #=====ADDRESS FIELDS===== def add_address(xml, payment_source, options) @@ -536,9 +574,9 @@ def add_customer_address(xml, options) end def billing_name(payment_source, options) - if payment_source&.name.present? + if !payment_source.is_a?(String) && payment_source&.name.present? payment_source.name[0..29] - elsif options[:billing_address][:name].present? + elsif options[:billing_address] && options[:billing_address][:name].present? options[:billing_address][:name][0..29] end end @@ -555,10 +593,17 @@ def get_address(options) options[:billing_address] || options[:address] end + def add_safetech_token_data(xml, payment_source, options) + payment_source_token = split_authorization(payment_source).values_at(2).first + xml.tag! :CardBrand, options[:card_brand] + xml.tag! :AccountNum, payment_source_token + end + #=====PAYMENT SOURCE FIELDS===== # Payment can be done through either Credit Card or Electronic Check def add_payment_source(xml, payment_source, options = {}) + add_safetech_token_data(xml, payment_source, options) if payment_source.is_a?(String) payment_source.is_a?(Check) ? add_echeck(xml, payment_source, options) : add_credit_card(xml, payment_source, options) end @@ -576,10 +621,10 @@ def add_echeck(xml, check, options = {}) end def add_credit_card(xml, credit_card, options) - xml.tag! :AccountNum, credit_card.number if credit_card - xml.tag! :Exp, expiry_date(credit_card) if credit_card + xml.tag! :AccountNum, credit_card.number if credit_card.is_a?(CreditCard) + xml.tag! :Exp, expiry_date(credit_card) if credit_card.is_a?(CreditCard) add_currency_fields(xml, options[:currency]) - add_verification_value(xml, credit_card) if credit_card + add_verification_value(xml, credit_card) if credit_card.is_a?(CreditCard) end def add_verification_value(xml, credit_card) @@ -594,7 +639,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 discover diners_club).include?(credit_card.brand) && bin == '000001' + xml.tag! :CardSecValInd, '1' if %w(visa discover diners_club).include?(credit_card.brand) xml.tag! :CardSecVal, credit_card.verification_value end @@ -886,13 +931,19 @@ def commit(order, message_type, retry_logic = nil, trace_number = nil) request.call(remote_url(:secondary)) end - Response.new(success?(response, message_type), message_from(response), response, + authorization = authorization_string(response[:tx_ref_num], response[:order_id], response[:safetech_token], response[:card_brand]) + + Response.new( + success?(response, message_type), + message_from(response), + response, { - authorization: authorization_string(response[:tx_ref_num], response[:order_id]), + authorization: authorization, 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) @@ -981,35 +1032,34 @@ def build_new_order_xml(action, money, payment_source, parameters = {}) xml.tag! :OrderID, format_order_id(parameters[:order_id]) xml.tag! :Amount, amount(money) xml.tag! :Comments, parameters[:comments] if parameters[:comments] - add_level2_tax(xml, parameters) add_level2_advice_addendum(xml, parameters) - add_aav(xml, payment_source, three_d_secure) # CustomerAni, AVSPhoneType and AVSDestPhoneType could be added here. - add_soft_descriptors(xml, parameters[:soft_descriptors]) - 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, 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 && payment_source.nil? - add_level2_purchase(xml, parameters) add_level3_purchase(xml, parameters) add_level3_tax(xml, parameters) add_line_items(xml, parameters) if parameters[:line_items] - add_ecp_details(xml, payment_source, parameters) if payment_source.is_a?(Check) add_card_indicators(xml, parameters) + 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_level2_card_and_more_tax(xml, parameters) + 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) + add_ecp_details(xml, payment_source, parameters) if payment_source.is_a?(Check) + add_stored_credentials(xml, parameters) add_pymt_brand_program_code(xml, payment_source, three_d_secure) + xml.tag! :TokenTxnType, parameters[:token_txn_type] if parameters[:token_txn_type] add_mastercard_fields(xml, payment_source, parameters, three_d_secure) if mastercard?(payment_source) + add_card_commodity_code(xml, parameters) + add_level3_vat_fields(xml, parameters) end end xml.target! @@ -1031,6 +1081,9 @@ def build_mark_for_capture_xml(money, authorization, parameters = {}) add_level3_purchase(xml, parameters) add_level3_tax(xml, parameters) add_line_items(xml, parameters) if parameters[:line_items] + add_level2_card_and_more_tax(xml, parameters) + add_card_commodity_code(xml, parameters) + add_level3_vat_fields(xml, parameters) end end xml.target! diff --git a/lib/active_merchant/billing/gateways/orbital/orbital_soft_descriptors.rb b/lib/active_merchant/billing/gateways/orbital/orbital_soft_descriptors.rb index 5b7f4517a69..bedc1b942b2 100644 --- a/lib/active_merchant/billing/gateways/orbital/orbital_soft_descriptors.rb +++ b/lib/active_merchant/billing/gateways/orbital/orbital_soft_descriptors.rb @@ -33,9 +33,7 @@ def validate errors << [:merchant_phone, 'is required to follow "NNN-NNN-NNNN" or "NNN-AAAAAAA" format'] if !empty?(self.merchant_phone) && !self.merchant_phone.match(PHONE_FORMAT_1) && !self.merchant_phone.match(PHONE_FORMAT_2) %i[merchant_email merchant_url].each do |attr| - unless self.send(attr).blank? - errors << [attr, 'is required to be 13 bytes or less'] if self.send(attr).bytesize > 13 - end + errors << [attr, 'is required to be 13 bytes or less'] if !self.send(attr).blank? && (self.send(attr).bytesize > 13) end errors_hash(errors) diff --git a/lib/active_merchant/billing/gateways/pac_net_raven.rb b/lib/active_merchant/billing/gateways/pac_net_raven.rb index 2cb24b07107..67b4a9a1ddb 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) @@ -178,8 +182,7 @@ def post_data(action, parameters = {}) post['RequestID'] = request_id post['Signature'] = signature(action, post, parameters) - request = post.merge(parameters).collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') - request + post.merge(parameters).collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end def timestamp 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 53203d51f96..d4a159d1d87 100644 --- a/lib/active_merchant/billing/gateways/pay_trace.rb +++ b/lib/active_merchant/billing/gateways/pay_trace.rb @@ -1,7 +1,7 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: class PayTraceGateway < Gateway - self.test_url = 'https://api.paytrace.com' + self.test_url = 'https://api.sandbox.paytrace.com' self.live_url = 'https://api.paytrace.com' self.supported_countries = ['US'] @@ -46,7 +46,7 @@ class PayTraceGateway < Gateway def initialize(options = {}) requires!(options, :username, :password, :integrator_id) super - acquire_access_token + acquire_access_token unless options[:access_token] end def purchase(money, payment_or_customer_id, options = {}) @@ -169,28 +169,35 @@ def scrub(transcript) transcript. gsub(%r((Authorization: Bearer )[a-zA-Z0-9:_]+), '\1[FILTERED]'). gsub(%r(("credit_card\\?":{\\?"number\\?":\\?")\d+), '\1[FILTERED]'). - gsub(%r(("cvv\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("csc\\?":\\?")\d+), '\1[FILTERED]'). gsub(%r(("username\\?":\\?")\w+@+\w+.+\w+), '\1[FILTERED]'). + gsub(%r(("username\\?":\\?")\w+), '\1[FILTERED]'). gsub(%r(("password\\?":\\?")\w+), '\1[FILTERED]'). gsub(%r(("integrator_id\\?":\\?")\w+), '\1[FILTERED]') end def acquire_access_token post = {} + base_url = (test? ? test_url : live_url) post[:grant_type] = 'password' post[:username] = @options[:username] post[:password] = @options[:password] data = post.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') - url = live_url + '/oauth/token' + url = base_url + '/oauth/token' oauth_headers = { 'Accept' => '*/*', 'Content-Type' => 'application/x-www-form-urlencoded' } response = ssl_post(url, data, oauth_headers) - json_response = JSON.parse(response) + json_response = parse(response) - @options[:access_token] = json_response['access_token'] if json_response['access_token'] - response + if json_response.include?('error') + oauth_response = Response.new(false, json_response['error_description']) + raise OAuthResponseError.new(oauth_response) + else + @options[:access_token] = json_response['access_token'] if json_response['access_token'] + response + end end private @@ -237,11 +244,11 @@ def visa_or_mastercard?(options) end def customer_id?(payment_or_customer_id) - payment_or_customer_id.class == String + payment_or_customer_id.instance_of?(String) end def string_literal_to_boolean(value) - return value unless value.class == String + return value unless value.instance_of?(String) if value.casecmp('true').zero? true @@ -258,15 +265,16 @@ def add_customer_data(post, options) end def add_address(post, creditcard, options) - return unless options[:billing_address] || options[:address] - - address = options[:billing_address] || options[:address] post[:billing_address] = {} + + if (address = options[:billing_address] || options[:address]) + post[:billing_address][:street_address] = address[:address1] + post[:billing_address][:city] = address[:city] + post[:billing_address][:state] = address[:state] + post[:billing_address][:zip] = address[:zip] + end + post[:billing_address][:name] = creditcard.name - post[:billing_address][:street_address] = address[:address1] - post[:billing_address][:city] = address[:city] - post[:billing_address][:state] = address[:state] - post[:billing_address][:zip] = address[:zip] end def add_amount(post, money, options) @@ -283,6 +291,7 @@ def add_payment(post, payment) post[:credit_card][:number] = payment.number post[:credit_card][:expiration_month] = payment.month post[:credit_card][:expiration_year] = payment.year + post[:csc] = payment.verification_value end end @@ -373,6 +382,12 @@ def commit(action, parameters) url = base_url + '/v1/' + action raw_response = ssl_post(url, post_data(parameters), headers) response = parse(raw_response) + handle_final_response(action, response) + rescue JSON::ParserError + unparsable_response(raw_response) + end + + def handle_final_response(action, response) success = success_from(response) Response.new( @@ -385,8 +400,6 @@ def commit(action, parameters) test: test?, error_code: success ? nil : error_code_from(response) ) - rescue JSON::ParserError - unparsable_response(raw_response) end def unparsable_response(raw_response) diff --git a/lib/active_merchant/billing/gateways/payeezy.rb b/lib/active_merchant/billing/gateways/payeezy.rb index b27eb7edac2..50248894423 100644 --- a/lib/active_merchant/billing/gateways/payeezy.rb +++ b/lib/active_merchant/billing/gateways/payeezy.rb @@ -35,12 +35,15 @@ def purchase(amount, payment_method, options = {}) add_invoice(params, options) add_reversal_id(params, options) + add_customer_ref(params, options) + add_reference_3(params, options) add_payment_method(params, payment_method, options) add_address(params, options) add_amount(params, amount, 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 @@ -50,12 +53,15 @@ def authorize(amount, payment_method, options = {}) add_invoice(params, options) add_reversal_id(params, options) + add_customer_ref(params, options) + add_reference_3(params, options) add_payment_method(params, payment_method, options) add_address(params, options) add_amount(params, amount, 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 @@ -140,6 +146,26 @@ def scrub(transcript) 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 @@ -148,6 +174,14 @@ def add_reversal_id(params, options) params[:reversal_id] = options[:reversal_id] if options[:reversal_id] end + def add_customer_ref(params, options) + params[:customer_ref] = options[:customer_ref] if options[:customer_ref] + end + + def add_reference_3(params, options) + params[:reference_3] = options[:reference_3] if options[:reference_3] + end + def amount_from_authorization(authorization) authorization.split('|').last.to_i end @@ -155,6 +189,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] @@ -168,7 +204,7 @@ def add_creditcard_for_tokenization(params, payment_method, options) params[:apikey] = @options[:apikey] params[:ta_token] = options[:ta_token] params[:type] = 'FDToken' - params[:credit_card] = add_card_data(payment_method) + params[:credit_card] = add_card_data(payment_method, options) params[:auth] = 'false' end @@ -184,7 +220,7 @@ def add_payment_method(params, payment_method, options) elsif payment_method.is_a? NetworkTokenizationCreditCard add_network_tokenization(params, payment_method, options) else - add_creditcard(params, payment_method) + add_creditcard(params, payment_method, options) end end @@ -195,7 +231,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] @@ -221,17 +257,17 @@ def add_token(params, payment_method, options) params[:token] = token end - def add_creditcard(params, creditcard) - credit_card = add_card_data(creditcard) + def add_creditcard(params, creditcard, options) + credit_card = add_card_data(creditcard, options) params[:method] = 'credit_card' params[:credit_card] = credit_card end - def add_card_data(payment_method) + def add_card_data(payment_method, options = {}) card = {} card[:type] = CREDIT_CARD_BRAND[payment_method.brand] - card[:cardholder_name] = payment_method.name + card[:cardholder_name] = name_from_payment_method(payment_method) || name_from_address(options) card[:card_number] = payment_method.number card[:exp_date] = format_exp_date(payment_method.month, payment_method.year) card[:cvv] = payment_method.verification_value if payment_method.verification_value? @@ -241,17 +277,17 @@ def add_card_data(payment_method) def add_network_tokenization(params, payment_method, options) nt_card = {} nt_card[:type] = 'D' - nt_card[:cardholder_name] = payment_method.first_name || name_from_address(options) + 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 - nt_card[:cavv] = payment_method.payment_cryptogram + 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 + params[:eci_indicator] = payment_method.eci.nil? ? '5' : payment_method.eci end def format_exp_date(month, year) @@ -263,6 +299,12 @@ def name_from_address(options) 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 @@ -305,8 +347,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) @@ -383,8 +424,7 @@ def generate_hmac(nonce, current_timestamp, payload) @options[:token], payload ].join('') - hash = Base64.strict_encode64(OpenSSL::HMAC.hexdigest('sha256', @options[:apisecret], message)) - hash + Base64.strict_encode64(OpenSSL::HMAC.hexdigest('sha256', @options[:apisecret], message)) end def headers(payload) 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/payflow.rb b/lib/active_merchant/billing/gateways/payflow.rb index 23f66fd65e9..949a42a2721 100644 --- a/lib/active_merchant/billing/gateways/payflow.rb +++ b/lib/active_merchant/billing/gateways/payflow.rb @@ -192,9 +192,7 @@ def build_credit_card_request(action, money, credit_card, options) xml.tag! 'TotalAmt', amount(money), 'Currency' => options[:currency] || currency(money) end - if %i(authorization purchase).include? action - add_mpi_3ds(xml, options[:three_d_secure]) if options[:three_d_secure] - end + add_mpi_3ds(xml, options[:three_d_secure]) if %i(authorization purchase).include?(action) && (options[:three_d_secure]) xml.tag! 'Tender' do add_credit_card(xml, credit_card, options) diff --git a/lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb b/lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb index ef51f210ece..7fe5009259a 100644 --- a/lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb +++ b/lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb @@ -99,7 +99,7 @@ def build_request(body, options = {}) end xml.tag! 'RequestAuth' do xml.tag! 'UserPass' do - xml.tag! 'User', !@options[:user].blank? ? @options[:user] : @options[:login] + xml.tag! 'User', @options[:user].blank? ? @options[:login] : @options[:user] xml.tag! 'Password', @options[:password] end end 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..19677797ff2 100644 --- a/lib/active_merchant/billing/gateways/paymentez.rb +++ b/lib/active_merchant/billing/gateways/paymentez.rb @@ -34,7 +34,7 @@ class PaymentezGateway < Gateway #:nodoc: 28 => :card_declined }.freeze - SUCCESS_STATUS = ['success', 'pending', 1, 0] + SUCCESS_STATUS = ['APPROVED', 'PENDING', 'pending', 'success', 1, 0] CARD_MAPPING = { 'visa' => 'vi', @@ -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 @@ -214,7 +218,7 @@ def add_external_mpi_fields(extra_params, options) xid: three_d_secure_options[:xid], eci: three_d_secure_options[:eci], version: three_d_secure_options[:version], - reference_id: three_d_secure_options[:three_ds_server_trans_id], + reference_id: three_d_secure_options[:ds_transaction_id], status: three_d_secure_options[:authentication_response_status] || three_d_secure_options[:directory_response_status] }.compact @@ -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 @@ -250,7 +262,7 @@ def commit_raw(object, action, parameters) def commit_transaction(action, parameters) response = commit_raw('transaction', action, parameters) Response.new( - success_from(response), + success_from(response, action), message_from(response), response, authorization: authorization_from(response), @@ -278,10 +290,22 @@ def headers } end - def success_from(response) - return false if response.include?('error') + def success_from(response, action = nil) + transaction_current_status = response.dig('transaction', 'current_status') + request_status = response['status'] + transaction_status = response.dig('transaction', 'status') + default_response = SUCCESS_STATUS.include?(transaction_current_status || request_status || transaction_status) - SUCCESS_STATUS.include?(response['status'] || response['transaction']['status']) + case action + when 'refund' + if transaction_current_status && request_status + transaction_current_status&.upcase == 'CANCELLED' && request_status&.downcase == 'success' + else + default_response + end + else + default_response + end end def card_success_from(response) @@ -302,10 +326,10 @@ def message_from(response) end def card_message_from(response) - if !response.include?('error') - response['message'] || response['card']['message'] - else + if response.include?('error') response['error']['type'] + else + response['message'] || response['card']['message'] end end 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 d228a7ca774..a7cff9fe813 100644 --- a/lib/active_merchant/billing/gateways/paysafe.rb +++ b/lib/active_merchant/billing/gateways/paysafe.rb @@ -124,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 @@ -138,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 @@ -401,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..ff0ab53d361 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) @@ -151,8 +155,7 @@ def post_data(action, parameters = {}) post[:password] = @options[:password] post[:type] = action - request = post.merge(parameters).collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') - request + post.merge(parameters).collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end end end diff --git a/lib/active_merchant/billing/gateways/paystation.rb b/lib/active_merchant/billing/gateways/paystation.rb index e7557ce5419..ddea5f241bc 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, - test: (response[:tm]&.casecmp('t')&.zero?), - authorization: response[:paystation_transaction_id]) + PaystationResponse.new( + success?(response), + message, + response, + test: response[:tm]&.casecmp('t')&.zero?, + 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 89e52e494a9..3ac30eec018 100644 --- a/lib/active_merchant/billing/gateways/payu_latam.rb +++ b/lib/active_merchant/billing/gateways/payu_latam.rb @@ -455,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..d3a9fa61c8c 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' @@ -224,7 +224,7 @@ def success_from(response) def error_code_from(response) return '' if success_from(response) - error = !STANDARD_ERROR_CODE_MAPPING[response['paywayCode']].nil? ? STANDARD_ERROR_CODE_MAPPING[response['paywayCode']] : STANDARD_ERROR_CODE[:processing_error] + error = STANDARD_ERROR_CODE_MAPPING[response['paywayCode']].nil? ? STANDARD_ERROR_CODE[:processing_error] : STANDARD_ERROR_CODE_MAPPING[response['paywayCode']] return error end diff --git a/lib/active_merchant/billing/gateways/pin.rb b/lib/active_merchant/billing/gateways/pin.rb index b054ed3b7a6..0562ff14134 100644 --- a/lib/active_merchant/billing/gateways/pin.rb +++ b/lib/active_merchant/billing/gateways/pin.rb @@ -31,6 +31,7 @@ def purchase(money, creditcard, options = {}) add_capture(post, options) add_metadata(post, options) add_3ds(post, options) + add_platform_adjustment(post, options) commit(:post, 'charges', post, options) end @@ -175,6 +176,10 @@ def add_metadata(post, options) post[:metadata] = options[:metadata] if options[:metadata] end + def add_platform_adjustment(post, options) + post[:platform_adjustment] = options[:platform_adjustment] if options[:platform_adjustment] + end + def add_3ds(post, options) if options[:three_d_secure] post[:three_d_secure] = {} diff --git a/lib/active_merchant/billing/gateways/plexo.rb b/lib/active_merchant/billing/gateways/plexo.rb index f4558e1c4df..d0bf2448ffc 100644 --- a/lib/active_merchant/billing/gateways/plexo.rb +++ b/lib/active_merchant/billing/gateways/plexo.rb @@ -67,6 +67,7 @@ def void(authorization, options = {}) def verify(credit_card, options = {}) post = {} post[:ReferenceId] = options[:reference_id] || generate_unique_id + post[:Flow] = 'direct' 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] @@ -76,6 +77,7 @@ def verify(credit_card, options = {}) add_metadata(post, options[:metadata]) add_amount(money, post, options) add_browser_details(post, options) + add_invoice_number(post, options) commit('/verify', post, options) end @@ -90,7 +92,8 @@ def scrub(transcript) 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]') + gsub(%r(("MerchantId\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("Cryptogram\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]') end private @@ -105,12 +108,14 @@ def build_auth_purchase_request(money, post, payment, options) 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] + post[:Flow] = 'direct' 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) + add_invoice_number(post, options) end def header(parameters = {}) @@ -186,6 +191,10 @@ def add_browser_details(post, browser_details) post[:BrowserDetails][:IpAddress] = browser_details[:ip] if browser_details[:ip] end + def add_invoice_number(post, options) + post[:InvoiceNumber] = options[:invoice_number] if options[:invoice_number] + end + def add_payment_method(post, payment, options) post[:paymentMethod] = {} @@ -199,6 +208,7 @@ def add_payment_method(post, payment, options) add_card_holder(post[:paymentMethod][:Card], payment, options) end + post[:paymentMethod][:Card][:Cryptogram] = payment.payment_cryptogram if payment&.is_a?(NetworkTokenizationCreditCard) end def add_card_holder(card, payment, options) 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 cddde41395c..762a7675726 100644 --- a/lib/active_merchant/billing/gateways/priority.rb +++ b/lib/active_merchant/billing/gateways/priority.rb @@ -55,7 +55,8 @@ def purchase(amount, credit_card, options = {}) add_merchant_id(params) add_amount(params, amount, options) - add_auth_purchase_params(params, credit_card, options) + add_auth_purchase_params(params, options) + add_credit_card(params, credit_card, 'purchase', options) commit('purchase', params: params) end @@ -67,7 +68,8 @@ def authorize(amount, credit_card, options = {}) add_merchant_id(params) add_amount(params, amount, options) - add_auth_purchase_params(params, credit_card, options) + add_auth_purchase_params(params, options) + add_credit_card(params, credit_card, 'purchase', options) commit('purchase', params: params) end @@ -100,7 +102,7 @@ def capture(amount, authorization, options = {}) add_merchant_id(params) add_amount(params, amount, options) params['paymentToken'] = payment_token(authorization) || options[:payment_token] - params['tenderType'] = options[:tender_type].present? ? options[:tender_type] : 'Card' + add_auth_purchase_params(params, options) commit('capture', params: params) end @@ -150,9 +152,8 @@ def add_merchant_id(params) params['merchantId'] = @options[:merchant_id] end - def add_auth_purchase_params(params, credit_card, options) + def add_auth_purchase_params(params, options) add_replay_id(params, options) - add_credit_card(params, credit_card, 'purchase', options) add_purchases_data(params, options) add_shipping_data(params, options) add_pos_data(params, options) 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..308a873b7ca 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. @@ -128,8 +128,8 @@ def scrub(transcript) gsub(%r((oauth_nonce=\")\w+), '\1[FILTERED]'). gsub(%r((oauth_signature=\")[a-zA-Z%0-9]+), '\1[FILTERED]'). gsub(%r((oauth_token=\")\w+), '\1[FILTERED]'). - gsub(%r((number\D+)\d{16}), '\1[FILTERED]'). - gsub(%r((cvc\D+)\d{3}), '\1[FILTERED]'). + gsub(%r((number\\\":\\\")\d+), '\1[FILTERED]'). + gsub(%r((cvc\\\":\\\")\d+), '\1[FILTERED]'). gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). gsub(%r((access_token\\?":\\?")[\w\-\.]+)i, '\1[FILTERED]'). gsub(%r((refresh_token\\?":\\?")\w+), '\1[FILTERED]'). @@ -263,7 +263,7 @@ def headers(method, uri) oauth_parameters[:oauth_signature] = CGI.escape(Base64.encode64(hmac_signature).chomp.delete("\n")) # prepare Authorization header string - oauth_parameters = Hash[oauth_parameters.sort_by { |k, _| k }] + oauth_parameters = oauth_parameters.sort_by { |k, _| k }.to_h oauth_headers = ["OAuth realm=\"#{@options[:realm]}\""] oauth_headers += oauth_parameters.map { |k, v| "#{k}=\"#{v}\"" } @@ -358,6 +358,7 @@ def extract_response_body_or_raise(response_error) rescue JSON::ParserError raise response_error end + response_error.response.body end diff --git a/lib/active_merchant/billing/gateways/quickpay/quickpay_v10.rb b/lib/active_merchant/billing/gateways/quickpay/quickpay_v10.rb index 91eeeff564e..3061ba3305c 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) @@ -251,7 +255,7 @@ def map_address(address) requires!(address, :name, :address1, :city, :zip, :country) country = Country.find(address[:country]) - mapped = { + { name: address[:name], street: address[:address1], city: address[:city], @@ -259,7 +263,6 @@ def map_address(address) zip_code: address[:zip], country_code: country.code(:alpha3).value } - mapped end def format_order_id(order_id) 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 6cdd07b2792..e99b8c10eb7 100644 --- a/lib/active_merchant/billing/gateways/rapyd.rb +++ b/lib/active_merchant/billing/gateways/rapyd.rb @@ -1,16 +1,23 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: class RapydGateway < Gateway + class_attribute :payment_redirect_test, :payment_redirect_live + self.test_url = 'https://sandboxapi.rapyd.net/v1/' self.live_url = 'https://api.rapyd.net/v1/' + self.payment_redirect_test = 'https://sandboxpayment-redirect.rapyd.net/v1/' + self.payment_redirect_live = 'https://payment-redirect.rapyd.net/v1/' + 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 = {}) @@ -61,11 +68,11 @@ def verify(credit_card, options = {}) def store(payment, options = {}) post = {} add_payment(post, payment, options) - add_customer_object(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) + add_payment_urls(post, options, 'store') add_address(post, payment, options) commit(:post, 'customers', post) end @@ -98,13 +105,18 @@ def add_reference(authorization) 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_ewallet(post, options) add_payment_fields(post, options) add_payment_urls(post, options) - add_customer_id(post, options) + add_idempotency(options) + end + + def add_idempotency(options) + @options[:idempotency] = options[:idempotency_key] if options[:idempotency_key] end def add_address(post, creditcard, options) @@ -125,6 +137,10 @@ def add_address(post, creditcard, options) def add_invoice(post, money, options) 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] + post[:requested_currency] = options[:requested_currency] if options[:requested_currency].present? + post[:fixed_side] = options[:fixed_side] if options[:fixed_side].present? + post[:expiration] = (options[:expiration_days] || 7).to_i.days.from_now.to_i if options[:fixed_side].present? end def add_payment(post, payment, options) @@ -133,15 +149,27 @@ def add_payment(post, payment, options) elsif payment.is_a?(Check) add_ach(post, payment, options) else - add_token(post, payment, options) + add_tokens(post, payment, options) end end def add_stored_credential(post, options) - return unless stored_credential = options[:stored_credential] + 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] - post[:payment_method][:fields][:network_reference_id] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id] - post[:initiation_type] = stored_credential[:reason_type] if stored_credential[:reason_type] + initiation_type = options[:initiation_type] || options[:stored_credential][:reason_type] + post[:initiation_type] = initiation_type if initiation_type end def add_creditcard(post, payment, options) @@ -151,14 +179,23 @@ def add_creditcard(post, payment, options) post[:payment_method][:type] = options[:pm_type] pm_fields[:number] = payment.number - pm_fields[:expiration_month] = payment.month.to_s - pm_fields[:expiration_year] = payment.year.to_s + pm_fields[:expiration_month] = format(payment.month, :two_digits).to_s + pm_fields[:expiration_year] = format(payment.year, :two_digits).to_s pm_fields[:name] = "#{payment.first_name} #{payment.last_name}" - pm_fields[:cvv] = payment.verification_value.to_s - + pm_fields[:cvv] = payment.verification_value.to_s unless valid_network_transaction_id?(options) || payment.verification_value.blank? + pm_fields[:recurrence_type] = options[:recurrence_type] if options[:recurrence_type] 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) post[:payment_method] = {} post[:payment_method][:fields] = {} @@ -172,22 +209,27 @@ def add_ach(post, payment, options) post[:payment_method][:fields][:payment_purpose] = options[:payment_purpose] if options[:payment_purpose] end - def add_token(post, payment, options) - return unless token = payment.split('|')[1] + def add_tokens(post, payment, options) + return unless payment.respond_to?(:split) - post[:payment_method] = token + 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) - return unless 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] + if options[:execute_threed] == true + post[:payment_method_options] = { '3d_required' => true } if options[:force_3d_secure].to_s == '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) @@ -195,25 +237,66 @@ def add_metadata(post, options) 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[:payment] = {} + post[:description] = options[:description] if options[:description] + post[:statement_descriptor] = options[:statement_descriptor] if options[:statement_descriptor] + end - post[:payment][:description] = options[:description] if options[:description] - post[:payment][:statement_descriptor] = options[:statement_descriptor] if options[:statement_descriptor] + 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[:receipt_email] = options[:email] if payment.is_a?(String) && options[:customer_id].present? && !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 add_payment_urls(post, options) - post[:complete_payment_url] = options[:complete_payment_url] if options[:complete_payment_url] - post[:error_payment_url] = options[:error_payment_url] if options[:error_payment_url] + 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[:email] = options[:email] unless payment.is_a?(String) && options[:customer_id].blank? + customer_data[:addresses] = [customer_address] if customer_address + customer_data end - def add_customer_object(post, payment, options) - post[:name] = "#{payment.first_name} #{payment.last_name}" - post[:phone_number] = options[:billing_address][:phone].gsub(/\D/, '') if options[:billing_address] - post[:email] = options[:email] if options[:email] + 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) @@ -223,13 +306,21 @@ def add_customer_id(post, options) def parse(body) return {} if body.empty? || body.nil? - JSON.parse(body) + parsed = JSON.parse(body) + parsed.is_a?(Hash) ? parsed : { 'status' => { 'status' => parsed } } + end + + def url(action, url_override = nil) + if url_override.to_s == 'payment_redirect' && action == 'payments' + (self.test? ? self.payment_redirect_test : self.payment_redirect_live) + action.to_s + else + (self.test? ? self.test_url : self.live_url) + action.to_s + end end def commit(method, action, parameters) - url = (test? ? test_url : live_url) + action.to_s rel_path = "#{method}/v1/#{action}" - response = api_request(method, url, rel_path, parameters) + response = api_request(method, url(action, @options[:url_override]), rel_path, parameters) Response.new( success_from(response), @@ -241,10 +332,25 @@ def commit(method, action, parameters) test: test?, error_code: error_code_from(response) ) + rescue ActiveMerchant::ResponseError => e + response = e.response.body.present? ? parse(e.response.body) : { 'status' => { 'response_code' => e.response.msg } } + message = response['status'].slice('message', 'response_code').values.select(&:present?).first || '' + Response.new(false, message, response, test: test?, error_code: error_code_from(response)) + 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 @@ -256,14 +362,14 @@ def headers(rel_path, payload) 'access_key' => @options[:access_key], 'salt' => salt, 'timestamp' => timestamp, - 'signature' => generate_hmac(rel_path, salt, timestamp, payload) - } + 'signature' => generate_hmac(rel_path, salt, timestamp, payload), + 'idempotency' => @options[:idempotency] + }.delete_if { |_, value| value.nil? } end def generate_hmac(rel_path, salt, timestamp, payload) signature = "#{rel_path}#{salt}#{timestamp}#{@options[:access_key]}#{@options[:secret_key]}#{payload}" - hash = Base64.urlsafe_encode64(OpenSSL::HMAC.hexdigest('sha256', @options[:secret_key], signature)) - hash + Base64.urlsafe_encode64(OpenSSL::HMAC.hexdigest('sha256', @options[:secret_key], signature)) end def avs_result(response) @@ -298,7 +404,7 @@ def authorization_from(response) end def error_code_from(response) - response.dig('status', 'error_code') unless success_from(response) + response.dig('status', 'error_code') || response.dig('status', 'response_code') || '' end def handle_response(response) diff --git a/lib/active_merchant/billing/gateways/reach.rb b/lib/active_merchant/billing/gateways/reach.rb index 092a19de698..5d6b9547b8d 100644 --- a/lib/active_merchant/billing/gateways/reach.rb +++ b/lib/active_merchant/billing/gateways/reach.rb @@ -4,7 +4,14 @@ class ReachGateway < Gateway self.test_url = 'https://checkout.rch.how/' self.live_url = 'https://checkout.rch.io/' - self.supported_countries = ['US'] + 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] @@ -71,10 +78,10 @@ def supports_scrubbing? def scrub(transcript) transcript. - gsub(%r(((MerchantId)[% \w]+[%]\d{2})[\w -]+), '\1[FILTERED]'). + 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') + 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 = {}) diff --git a/lib/active_merchant/billing/gateways/redsys.rb b/lib/active_merchant/billing/gateways/redsys.rb index 00239bf2ee2..a733b77bcdc 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 @@ -266,14 +266,7 @@ def refund(money, authorization, options = {}) end def verify(creditcard, 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 + purchase(0, creditcard, options) end def supports_scrubbing @@ -538,6 +531,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 @@ -695,8 +689,7 @@ def encrypt(key, order_id) order_id += "\0" until order_id.bytesize % block_length == 0 # Pad with zeros - output = cipher.update(order_id) + cipher.final - output + cipher.update(order_id) + cipher.final end def mac256(key, data) diff --git a/lib/active_merchant/billing/gateways/redsys_rest.rb b/lib/active_merchant/billing/gateways/redsys_rest.rb new file mode 100644 index 00000000000..3e8de87ed68 --- /dev/null +++ b/lib/active_merchant/billing/gateways/redsys_rest.rb @@ -0,0 +1,465 @@ +# coding: utf-8 + +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + # = Redsys Merchant Gateway + # + # Gateway support for the Spanish "Redsys" payment gateway system. This is + # used by many banks in Spain and is particularly well supported by + # Catalunya Caixa's ecommerce department. + # + # Redsys requires an order_id be provided with each transaction and it must + # follow a specific format. The rules are as follows: + # + # * First 4 digits must be numerical + # * Remaining 8 digits may be alphanumeric + # * Max length: 12 + # + # If an invalid order_id is provided, we do our best to clean it up. + # + # Written by Piers Chambers (Varyonic.com) + # + # *** SHA256 Authentication Update *** + # + # Redsys has dropped support for the SHA1 authentication method. + # Developer documentation: https://pagosonline.redsys.es/desarrolladores.html + class RedsysRestGateway < Gateway + self.test_url = 'https://sis-t.redsys.es:25443/sis/rest/' + self.live_url = 'https://sis.redsys.es/sis/rest/' + + self.supported_countries = ['ES'] + self.default_currency = 'EUR' + self.money_format = :cents + # Not all card types may be activated by the bank! + self.supported_cardtypes = %i[visa master american_express jcb diners_club unionpay] + self.homepage_url = 'http://www.redsys.es/' + self.display_name = 'Redsys (REST)' + + CURRENCY_CODES = { + 'AED' => '784', + 'ARS' => '32', + 'AUD' => '36', + 'BRL' => '986', + 'BOB' => '68', + 'CAD' => '124', + 'CHF' => '756', + 'CLP' => '152', + 'CNY' => '156', + 'COP' => '170', + 'CRC' => '188', + 'CZK' => '203', + 'DKK' => '208', + 'DOP' => '214', + 'EUR' => '978', + 'GBP' => '826', + 'GTQ' => '320', + 'HUF' => '348', + 'IDR' => '360', + 'INR' => '356', + 'JPY' => '392', + 'KRW' => '410', + 'MYR' => '458', + 'MXN' => '484', + 'NOK' => '578', + 'NZD' => '554', + 'PEN' => '604', + 'PLN' => '985', + 'RUB' => '643', + 'SAR' => '682', + 'SEK' => '752', + 'SGD' => '702', + 'THB' => '764', + 'TWD' => '901', + 'USD' => '840', + 'UYU' => '858' + } + + # The set of supported transactions for this gateway. + # More operations are supported by the gateway itself, but + # are not supported in this library. + SUPPORTED_TRANSACTIONS = { + purchase: '0', + authorize: '1', + capture: '2', + refund: '3', + cancel: '9', + verify: '7' + } + + # These are the text meanings sent back by the acquirer when + # a card has been rejected. Syntax or general request errors + # are not covered here. + RESPONSE_TEXTS = { + 0 => 'Transaction Approved', + 400 => 'Cancellation Accepted', + 481 => 'Cancellation Accepted', + 500 => 'Reconciliation Accepted', + 900 => 'Refund / Confirmation approved', + + 101 => 'Card expired', + 102 => 'Card blocked temporarily or under susciption of fraud', + 104 => 'Transaction not permitted', + 107 => 'Contact the card issuer', + 109 => 'Invalid identification by merchant or POS terminal', + 110 => 'Invalid amount', + 114 => 'Card cannot be used to the requested transaction', + 116 => 'Insufficient credit', + 118 => 'Non-registered card', + 125 => 'Card not effective', + 129 => 'CVV2/CVC2 Error', + 167 => 'Contact the card issuer: suspected fraud', + 180 => 'Card out of service', + 181 => 'Card with credit or debit restrictions', + 182 => 'Card with credit or debit restrictions', + 184 => 'Authentication error', + 190 => 'Refusal with no specific reason', + 191 => 'Expiry date incorrect', + 195 => 'Requires SCA authentication', + + 201 => 'Card expired', + 202 => 'Card blocked temporarily or under suspicion of fraud', + 204 => 'Transaction not permitted', + 207 => 'Contact the card issuer', + 208 => 'Lost or stolen card', + 209 => 'Lost or stolen card', + 280 => 'CVV2/CVC2 Error', + 290 => 'Declined with no specific reason', + + 480 => 'Original transaction not located, or time-out exceeded', + 501 => 'Original transaction not located, or time-out exceeded', + 502 => 'Original transaction not located, or time-out exceeded', + 503 => 'Original transaction not located, or time-out exceeded', + + 904 => 'Merchant not registered at FUC', + 909 => 'System error', + 912 => 'Issuer not available', + 913 => 'Duplicate transmission', + 916 => 'Amount too low', + 928 => 'Time-out exceeded', + 940 => 'Transaction cancelled previously', + 941 => 'Authorization operation already cancelled', + 942 => 'Original authorization declined', + 943 => 'Different details from origin transaction', + 944 => 'Session error', + 945 => 'Duplicate transmission', + 946 => 'Cancellation of transaction while in progress', + 947 => 'Duplicate tranmission while in progress', + 949 => 'POS Inoperative', + 950 => 'Refund not possible', + 9064 => 'Card number incorrect', + 9078 => 'No payment method available', + 9093 => 'Non-existent card', + 9218 => 'Recursive transaction in bad gateway', + 9253 => 'Check-digit incorrect', + 9256 => 'Preauth not allowed for merchant', + 9257 => 'Preauth not allowed for card', + 9261 => 'Operating limit exceeded', + 9912 => 'Issuer not available', + 9913 => 'Confirmation error', + 9914 => 'KO Confirmation' + } + + # Expected values as per documentation + THREE_DS_V2 = '2.1.0' + + # Creates a new instance + # + # Redsys requires a login and secret_key, and optionally also accepts a + # non-default terminal. + # + # ==== Options + # + # * :login -- The Redsys Merchant ID (REQUIRED) + # * :secret_key -- The Redsys Secret Key. (REQUIRED) + # * :terminal -- The Redsys Terminal. Defaults to 1. (OPTIONAL) + # * :test -- +true+ or +false+. Defaults to +false+. (OPTIONAL) + def initialize(options = {}) + requires!(options, :login, :secret_key) + options[:terminal] ||= 1 + options[:signature_algorithm] = 'sha256' + super + end + + def purchase(money, payment, options = {}) + requires!(options, :order_id) + + post = {} + add_action(post, :purchase, options) + add_amount(post, money, options) + add_order(post, options[:order_id]) + add_payment(post, payment) + add_description(post, options) + add_direct_payment(post, options) + add_threeds(post, options) + + commit(post, options) + end + + def authorize(money, payment, options = {}) + requires!(options, :order_id) + + post = {} + add_action(post, :authorize, options) + add_amount(post, money, options) + add_order(post, options[:order_id]) + add_payment(post, payment) + add_description(post, options) + add_direct_payment(post, options) + add_threeds(post, options) + + commit(post, options) + end + + def capture(money, authorization, options = {}) + post = {} + add_action(post, :capture) + add_amount(post, money, options) + order_id, = split_authorization(authorization) + add_order(post, order_id) + add_description(post, options) + + commit(post, options) + end + + def void(authorization, options = {}) + requires!(options, :order_id) + + post = {} + add_action(post, :cancel) + order_id, amount, currency = split_authorization(authorization) + add_amount(post, amount, currency: currency) + add_order(post, order_id) + add_description(post, options) + + commit(post, options) + end + + def refund(money, authorization, options = {}) + requires!(options, :order_id) + + post = {} + add_action(post, :refund) + add_amount(post, money, options) + order_id, = split_authorization(authorization) + add_order(post, order_id) + add_description(post, options) + + commit(post, options) + end + + def verify(creditcard, options = {}) + requires!(options, :order_id) + + post = {} + add_action(post, :verify, options) + add_amount(post, 0, options) + add_order(post, options[:order_id]) + add_payment(post, creditcard) + add_description(post, options) + add_direct_payment(post, options) + add_threeds(post, options) + + commit(post, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((PAN\"=>\")(\d+)), '\1[FILTERED]'). + gsub(%r((CVV2\"=>\")(\d+)), '\1[FILTERED]') + end + + private + + def add_direct_payment(post, options) + # Direct payment skips 3DS authentication. We should only apply this if execute_threed is false + # or authentication data is not present. Authentication data support to be added in the future. + return if options[:execute_threed] || options[:authentication_data] + + post[:DS_MERCHANT_DIRECTPAYMENT] = true + end + + def add_threeds(post, options) + return unless options[:execute_threed] || options[:three_ds_2] + + post[:DS_MERCHANT_EMV3DS] = if options[:execute_threed] + { threeDSInfo: 'CardData' } + else + add_browser_info(post, options) + end + end + + def add_browser_info(post, options) + return unless browser_info = options.dig(:three_ds_2, :browser_info) + + { + browserAcceptHeader: browser_info[:accept_header], + browserUserAgent: browser_info[:user_agent], + browserJavaEnabled: browser_info[:java], + browserJavascriptEnabled: browser_info[:java], + browserLanguage: browser_info[:language], + browserColorDepth: browser_info[:depth], + browserScreenHeight: browser_info[:height], + browserScreenWidth: browser_info[:width], + browserTZ: browser_info[:timezone] + } + end + + def add_action(post, action, options = {}) + post[:DS_MERCHANT_TRANSACTIONTYPE] = transaction_code(action) + end + + def add_amount(post, money, options) + post[:DS_MERCHANT_AMOUNT] = amount(money).to_s + post[:DS_MERCHANT_CURRENCY] = currency_code(options[:currency] || currency(money)) + end + + def add_description(post, options) + post[:DS_MERCHANT_PRODUCTDESCRIPTION] = CGI.escape(options[:description]) if options[:description] + end + + def add_order(post, order_id) + post[:DS_MERCHANT_ORDER] = clean_order_id(order_id) + end + + def add_payment(post, card) + name = [card.first_name, card.last_name].join(' ').slice(0, 60) + year = sprintf('%.4i', card.year) + month = sprintf('%.2i', card.month) + post['DS_MERCHANT_TITULAR'] = CGI.escape(name) + post['DS_MERCHANT_PAN'] = card.number + post['DS_MERCHANT_EXPIRYDATE'] = "#{year[2..3]}#{month}" + post['DS_MERCHANT_CVV2'] = card.verification_value if card.verification_value.present? + end + + def determine_action(options) + # If execute_threed is true, we need to use iniciaPeticionREST to set up authentication + # Otherwise we are skipping 3DS or we should have 3DS authentication results + options[:execute_threed] ? 'iniciaPeticionREST' : 'trataPeticionREST' + end + + def commit(post, options) + url = (test? ? test_url : live_url) + action = determine_action(options) + raw_response = parse(ssl_post(url + action, post_data(post, options))) + payload = raw_response['Ds_MerchantParameters'] + return Response.new(false, "#{raw_response['errorCode']} ERROR") unless payload + + response = JSON.parse(Base64.decode64(payload)).transform_keys!(&:downcase).with_indifferent_access + return Response.new(false, 'Unable to verify response') unless validate_signature(payload, raw_response['Ds_Signature'], response[:ds_order]) + + succeeded = success_from(response, options) + Response.new( + succeeded, + message_from(response), + response, + authorization: authorization_from(response), + test: test?, + error_code: succeeded ? nil : response[:ds_response] + ) + end + + def post_data(post, options) + add_authentication(post, options) + merchant_parameters = JSON.generate(post) + encoded_parameters = Base64.strict_encode64(merchant_parameters) + post_data = PostData.new + post_data['Ds_SignatureVersion'] = 'HMAC_SHA256_V1' + post_data['Ds_MerchantParameters'] = encoded_parameters + post_data['Ds_Signature'] = sign_request(encoded_parameters, post[:DS_MERCHANT_ORDER]) + post_data.to_post_data + end + + def add_authentication(post, options) + post[:DS_MERCHANT_TERMINAL] = options[:terminal] || @options[:terminal] + post[:DS_MERCHANT_MERCHANTCODE] = @options[:login] + end + + def parse(body) + JSON.parse(body) + end + + def success_from(response, options) + return true if response[:ds_emv3ds] && options[:execute_threed] + + # Need to get updated for 3DS support + if code = response[:ds_response] + (code.to_i < 100) || [400, 481, 500, 900].include?(code.to_i) + else + false + end + end + + def message_from(response) + return response.dig(:ds_emv3ds, :threeDSInfo) if response[:ds_emv3ds] + + code = response[:ds_response]&.to_i + code = 0 if code < 100 + RESPONSE_TEXTS[code] || 'Unknown code, please check in manual' + end + + def validate_signature(data, signature, order_number) + key = encrypt(@options[:secret_key], order_number) + Base64.urlsafe_encode64(mac256(key, data)) == signature + end + + def authorization_from(response) + # Need to get updated for 3DS support + [response[:ds_order], response[:ds_amount], response[:ds_currency]].join('|') + end + + def split_authorization(authorization) + order_id, amount, currency = authorization.split('|') + [order_id, amount.to_i, currency] + end + + def currency_code(currency) + return currency if currency =~ /^\d+$/ + raise ArgumentError, "Unknown currency #{currency}" unless CURRENCY_CODES[currency] + + CURRENCY_CODES[currency] + end + + def transaction_code(type) + SUPPORTED_TRANSACTIONS[type] + end + + def clean_order_id(order_id) + cleansed = order_id.gsub(/[^\da-zA-Z]/, '') + if /^\d{4}/.match?(cleansed) + cleansed[0..11] + else + '%04d' % [rand(0..9999)] + cleansed[0...8] + end + end + + def sign_request(encoded_parameters, order_id) + raise(ArgumentError, 'missing order_id') unless order_id + + key = encrypt(@options[:secret_key], order_id) + Base64.strict_encode64(mac256(key, encoded_parameters)) + end + + def encrypt(key, order_id) + block_length = 8 + cipher = OpenSSL::Cipher.new('DES3') + cipher.encrypt + + cipher.key = Base64.urlsafe_decode64(key) + # The OpenSSL default of an all-zeroes ("\\0") IV is used. + cipher.padding = 0 + + order_id += "\0" until order_id.bytesize % block_length == 0 # Pad with zeros + + cipher.update(order_id) + cipher.final + end + + def mac256(key, data) + OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, data) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/s5.rb b/lib/active_merchant/billing/gateways/s5.rb index 4dc62423313..51acce11b6b 100644 --- a/lib/active_merchant/billing/gateways/s5.rb +++ b/lib/active_merchant/billing/gateways/s5.rb @@ -128,9 +128,7 @@ def add_payment(xml, money, action, options) end def add_account(xml, payment_method) - if !payment_method.respond_to?(:number) - xml.Account(registration: payment_method) - else + if payment_method.respond_to?(:number) xml.Account do xml.Number payment_method.number xml.Holder "#{payment_method.first_name} #{payment_method.last_name}" @@ -138,6 +136,8 @@ def add_account(xml, payment_method) xml.Expiry(year: payment_method.year, month: payment_method.month) xml.Verification payment_method.verification_value end + else + xml.Account(registration: payment_method) end end diff --git a/lib/active_merchant/billing/gateways/safe_charge.rb b/lib/active_merchant/billing/gateways/safe_charge.rb index 49554a31454..3f522c0a1c0 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,8 +86,9 @@ 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 + options[:unreferenced_refund].to_s == 'true' ? post[:sg_CreditType] = 2 : post[:sg_CreditType] = 1 commit(post) end @@ -148,26 +149,46 @@ def add_transaction_data(trans_type, post, money, options) end def add_payment(post, payment, options = {}) - post[:sg_ExpMonth] = format(payment.month, :two_digits) - post[:sg_ExpYear] = format(payment.year, :two_digits) - post[:sg_CardNumber] = payment.number - - if payment.is_a?(NetworkTokenizationCreditCard) && payment.source == :network_token - post[:sg_CAVV] = payment.payment_cryptogram - post[:sg_ECI] = options[:three_d_secure] && options[:three_d_secure][:eci] || '05' - post[:sg_IsExternalMPI] = 1 - post[:sg_ExternalTokenProvider] = 5 - else - post[:sg_CVV2] = payment.verification_value - post[:sg_NameOnCard] = payment.name - post[:sg_StoredCredentialMode] = (options[:stored_credential_mode] == true ? 1 : 0) + case payment + when String + add_token(post, payment) + when CreditCard + post[:sg_ExpMonth] = format(payment.month, :two_digits) + post[:sg_ExpYear] = format(payment.year, :two_digits) + post[:sg_CardNumber] = payment.number + + if payment.is_a?(NetworkTokenizationCreditCard) && payment.source == :network_token + add_network_token(post, payment, options) + else + add_credit_card(post, payment, options) + end end end + def add_token(post, payment) + _, transaction_id, token = payment.split('|') + + post[:sg_TransactionID] = transaction_id + post[:sg_CCToken] = token + end + + def add_credit_card(post, payment, options) + post[:sg_CVV2] = payment.verification_value + post[:sg_NameOnCard] = payment.name + post[:sg_StoredCredentialMode] = (options[:stored_credential_mode] == true ? 1 : 0) + end + + def add_network_token(post, payment, options) + post[:sg_CAVV] = payment.payment_cryptogram + post[:sg_ECI] = options[:three_d_secure] && options[:three_d_secure][:eci] || '05' + post[:sg_IsExternalMPI] = 1 + post[:sg_ExternalTokenProvider] = 5 + end + def add_customer_details(post, payment, options) if address = options[:billing_address] || options[:address] - post[:sg_FirstName] = payment.first_name - post[:sg_LastName] = payment.last_name + post[:sg_FirstName] = payment.first_name if payment.respond_to?(:first_name) + post[:sg_LastName] = payment.last_name if payment.respond_to?(:last_name) post[:sg_Address] = address[:address1] if address[:address1] post[:sg_City] = address[:city] if address[:city] post[:sg_State] = address[:state] if address[:state] 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 24fff459089..5e38cf6e300 100644 --- a/lib/active_merchant/billing/gateways/sage_pay.rb +++ b/lib/active_merchant/billing/gateways/sage_pay.rb @@ -6,8 +6,8 @@ class SagePayGateway < Gateway class_attribute :simulator_url - self.test_url = 'https://test.sagepay.com/gateway/service' - self.live_url = 'https://live.sagepay.com/gateway/service' + self.test_url = 'https://sandbox.opayo.eu.elavon.com/gateway/service' + self.live_url = 'https://live.opayo.eu.elavon.com/gateway/service' self.simulator_url = 'https://test.sagepay.com/Simulator' APPROVED = 'OK' @@ -78,6 +78,7 @@ class SagePayGateway < Gateway def initialize(options = {}) requires!(options, :login) + @protocol_version = options.fetch(:protocol_version, '3.00') super end @@ -86,6 +87,9 @@ def purchase(money, payment_method, options = {}) post = {} + add_override_protocol_version(options) + add_three_ds_data(post, options) + add_stored_credentials_data(post, options) add_amount(post, money, options) add_invoice(post, options) add_payment_method(post, payment_method, options) @@ -101,6 +105,9 @@ def authorize(money, payment_method, options = {}) post = {} + add_three_ds_data(post, options) + add_stored_credentials_data(post, options) + add_override_protocol_version(options) add_amount(post, money, options) add_invoice(post, options) add_payment_method(post, payment_method, options) @@ -115,6 +122,7 @@ def authorize(money, payment_method, options = {}) def capture(money, identification, options = {}) post = {} + add_override_protocol_version(options) add_reference(post, identification) add_release_amount(post, money, options) @@ -124,6 +132,7 @@ def capture(money, identification, options = {}) def void(identification, options = {}) post = {} + add_override_protocol_version(options) add_reference(post, identification) action = abort_or_void_from(identification) @@ -136,6 +145,7 @@ def refund(money, identification, options = {}) post = {} + add_override_protocol_version(options) add_related_reference(post, identification) add_amount(post, money, options) add_invoice(post, options) @@ -150,6 +160,7 @@ def credit(money, identification, options = {}) def store(credit_card, options = {}) post = {} + add_override_protocol_version(options) add_credit_card(post, credit_card) add_currency(post, 0, options) @@ -158,6 +169,7 @@ def store(credit_card, options = {}) def unstore(token, options = {}) post = {} + add_override_protocol_version(options) add_token(post, token) commit(:unstore, post) end @@ -182,6 +194,58 @@ def scrub(transcript) private + def add_override_protocol_version(options) + @protocol_version = options[:protocol_version] if options[:protocol_version] + end + + def add_three_ds_data(post, options) + return unless @protocol_version == '4.00' + return unless three_ds_2_options = options[:three_ds_2] + + add_pair(post, :ThreeDSNotificationURL, three_ds_2_options[:notification_url]) + return unless three_ds_2_options[:browser_info] + + add_browser_info(post, three_ds_2_options[:browser_info]) + end + + def add_browser_info(post, browser_info) + add_pair(post, :BrowserAcceptHeader, browser_info[:accept_header]) + add_pair(post, :BrowserColorDepth, browser_info[:depth]) + add_pair(post, :BrowserJavascriptEnabled, format_boolean(browser_info[:java])) + add_pair(post, :BrowserJavaEnabled, format_boolean(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, :BrowserTZ, browser_info[:timezone]) + add_pair(post, :BrowserUserAgent, browser_info[:user_agent]) + add_pair(post, :ChallengeWindowSize, browser_info[:browser_size]) + end + + def add_stored_credentials_data(post, options) + return unless @protocol_version == '4.00' + return unless stored_credential = options[:stored_credential] + + initiator = stored_credential[:initiator] == 'cardholder' ? 'CIT' : 'MIT' + cof_usage = if stored_credential[:initial_transaction] && initiator == 'CIT' + 'FIRST' + elsif !stored_credential[:initial_transaction] && initiator == 'MIT' + 'SUBSEQUENT' + end + + add_pair(post, :COFUsage, cof_usage) if cof_usage + add_pair(post, :InitiatedTYPE, initiator) + add_pair(post, :SchemeTraceID, stored_credential[:network_transaction_id]) if stored_credential[:network_transaction_id] + + reasoning = stored_credential[:reason_type] == 'installment' ? 'instalment' : stored_credential[:reason_type] + add_pair(post, :MITType, reasoning.upcase) + + if %w(instalment recurring).any?(reasoning) + add_pair(post, :RecurringExpiry, options[:recurring_expiry]) + add_pair(post, :RecurringFrequency, options[:recurring_frequency]) + add_pair(post, :PurchaseInstalData, options[:installment_data]) + end + end + def truncate(value, max_size) return nil unless value return value.to_s if CGI.escape(value.to_s).length <= max_size @@ -346,14 +410,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) @@ -401,7 +469,7 @@ def post_data(action, parameters = {}) parameters.update( Vendor: @options[:login], TxType: TRANSACTIONS[action], - VPSProtocol: @options.fetch(:protocol_version, '3.00') + VPSProtocol: @protocol_version ) parameters.update(ReferrerID: application_id) if application_id && (application_id != Gateway.application_id) @@ -409,6 +477,12 @@ def post_data(action, parameters = {}) parameters.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end + def format_boolean(value) + return if value.nil? + + value ? '1' : '0' + end + # SagePay returns data in the following format # Key1=value1 # Key2=value2 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..e20a326e6c7 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) @@ -75,7 +79,7 @@ def fraud_review?(response) def parse(body) fields = split(body) - results = { + { response_code: fields[RESPONSE_CODE].to_i, response_reason_code: fields[RESPONSE_REASON_CODE], response_reason_text: fields[RESPONSE_REASON_TEXT], @@ -85,7 +89,6 @@ def parse(body) authorization_code: fields[AUTHORIZATION_CODE], cardholder_authentication_code: fields[CARDHOLDER_AUTH_CODE] } - results end def post_data(action, parameters = {}) @@ -101,8 +104,7 @@ def post_data(action, parameters = {}) post[:encap_char] = '$' post[:solution_ID] = application_id if application_id - request = post.merge(parameters).collect { |key, value| "x_#{key}=#{CGI.escape(value.to_s)}" }.join('&') - request + post.merge(parameters).collect { |key, value| "x_#{key}=#{CGI.escape(value.to_s)}" }.join('&') end def add_currency_code(post, money, options) 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..3d7225f37da 100644 --- a/lib/active_merchant/billing/gateways/securion_pay.rb +++ b/lib/active_merchant/billing/gateways/securion_pay.rb @@ -193,7 +193,8 @@ def add_creditcard(post, creditcard, options) post[:card] = card add_address(post, options) elsif creditcard.kind_of?(String) - post[:card] = creditcard + key = creditcard.match(/^pm_/) ? :paymentMethod : :card + post[key] = creditcard else raise ArgumentError.new("Unhandled payment method #{creditcard.class}.") end @@ -223,24 +224,37 @@ 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.dig('error', 'charge') || response.dig('error', 'chargeId')) + end + end + + def success?(response) + !response.key?('error') end def headers(options = {}) secret_key = options[:secret_key] || @options[:secret_key] - headers = { + { 'Authorization' => 'Basic ' + Base64.encode64(secret_key.to_s + ':').strip, 'User-Agent' => "SecurionPay/v1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}" } - headers end def response_error(raw_response) @@ -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' => { @@ -298,7 +312,7 @@ def json_error(raw_response) end def test? - (@options[:secret_key]&.include?('_test_')) + @options[:secret_key]&.include?('_test_') end end end diff --git a/lib/active_merchant/billing/gateways/shift4.rb b/lib/active_merchant/billing/gateways/shift4.rb index 7d638542355..407ca4253d3 100644 --- a/lib/active_merchant/billing/gateways/shift4.rb +++ b/lib/active_merchant/billing/gateways/shift4.rb @@ -20,22 +20,6 @@ class Shift4Gateway < Gateway '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) @@ -91,7 +75,7 @@ def capture(money, authorization, options = {}) commit(action, post, options) end - def refund(money, authorization, options = {}) + def refund(money, payment_method, options = {}) post = {} action = 'refund' @@ -99,12 +83,15 @@ def refund(money, authorization, options = {}) add_invoice(post, money, options) add_clerk(post, options) add_transaction(post, options) - add_card(action, post, get_card_token(authorization), 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) @@ -144,7 +131,7 @@ def scrub(transcript) 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]') + gsub(%r(("securityCode\\?":{\\?"\w+\\?":\d+,\\?"value\\?":\\?")\d*)i, '\1[FILTERED]') end def setup_access_token @@ -153,6 +140,8 @@ def setup_access_token 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 @@ -183,6 +172,7 @@ 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) @@ -257,6 +247,7 @@ def commit(action, parameters, option) message_from(action, response), response, authorization: authorization_from(action, response), + avs_result: avs_result_from(response), test: test?, error_code: error_code_from(action, response) ) @@ -278,13 +269,25 @@ def parse(body) end def message_from(action, response) - success_from(action, response) ? 'Transaction successful' : (error(response)&.dig('longText') || 'Transaction declined') + success_from(action, response) ? 'Transaction successful' : (error(response)&.dig('longText') || response['result'].first&.dig('transaction', 'hostResponse', 'reasonDescription') || 'Transaction declined') end def error_code_from(action, response) - return unless success_from(action, response) + code = response['result'].first&.dig('transaction', 'responseCode') + primary_code = response['result'].first['error'].present? + return unless code == 'D' || primary_code == true || success_from(action, response) + + if response['result'].first&.dig('transaction', 'hostResponse') + response['result'].first&.dig('transaction', 'hostResponse', 'reasonCode') + elsif response['result'].first['error'] + response['result'].first&.dig('error', 'primaryCode') + else + response['result'].first&.dig('transaction', 'responseCode') + end + end - STANDARD_ERROR_CODE_MAPPING[response['primaryCode']] + def avs_result_from(response) + AVSResult.new(code: response['result'].first&.dig('transaction', 'avs', 'result')) if response['result'].first&.dig('transaction', 'avs') end def authorization_from(action, response) @@ -306,7 +309,7 @@ def get_invoice(authorization) def request_headers(action, options) headers = { - 'Content-Type' => 'application/x-www-form-urlencoded' + 'Content-Type' => 'application/json' } headers['AccessToken'] = @access_token headers['Invoice'] = options[:invoice] if action != 'capture' && options[:invoice].present? 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..7af6542ec27 --- /dev/null +++ b/lib/active_merchant/billing/gateways/shift4_v2.rb @@ -0,0 +1,117 @@ +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 store(payment_method, options = {}) + post = case payment_method + when CreditCard + cc = {}.tap { |card| add_creditcard(card, payment_method, options) }[:card] + options[:customer_id].blank? ? { email: options[:email], card: cc } : cc + when Check + bank_account_object(payment_method, options) + else + raise ArgumentError.new("Unhandled payment method #{payment_method.class}.") + end + + commit url_for_store(payment_method, options), post, options + end + + def url_for_store(payment_method, options = {}) + case payment_method + when CreditCard + options[:customer_id].blank? ? 'customers' : "customers/#{options[:customer_id]}/cards" + when Check then 'payment-methods' + end + end + + def unstore(reference, options = {}) + commit("customers/#{options[:customer_id]}/cards/#{reference}", nil, options, :delete) + 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 + + def add_creditcard(post, payment_method, options) + return super unless payment_method.is_a?(Check) + + post[:paymentMethod] = bank_account_object(payment_method, options) + end + + def bank_account_object(payment_method, options) + { + type: :ach, + fraudCheckData: { + ipAddress: options[:ip], + email: options[:email] + }.compact, + billing: { + name: payment_method.name, + address: { country: options.dig(:billing_address, :country) } + }.compact, + ach: { + account: { + routingNumber: payment_method.routing_number, + accountNumber: payment_method.account_number, + accountType: get_account_type(payment_method) + }, + verificationProvider: :external + } + } + end + + def get_account_type(check) + holder = (check.account_holder_type || '').match(/business/i) ? :corporate : :personal + "#{holder}_#{check.account_type}" + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/simetrik.rb b/lib/active_merchant/billing/gateways/simetrik.rb index f8c34654200..877a2cdcf30 100644 --- a/lib/active_merchant/billing/gateways/simetrik.rb +++ b/lib/active_merchant/billing/gateways/simetrik.rb @@ -44,7 +44,7 @@ class SimetrikGateway < Gateway def initialize(options = {}) requires!(options, :client_id, :client_secret) super - @access_token = {} + @access_token = options[:access_token] || {} sign_access_token() end @@ -280,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 @@ -318,7 +318,7 @@ def message_from(response) end def url(action, url_params) - "#{(test? ? test_url : live_url)}/#{url_params[:token_acquirer]}/#{action}" + "#{test? ? test_url : live_url}/#{url_params[:token_acquirer]}/#{action}" end def post_data(data = {}) @@ -356,12 +356,18 @@ def fetch_access_token login_info[:client_secret] = @options[:client_secret] 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' - })) - @access_token[:access_token] = response['access_token'] - @access_token[:expires_at] = Time.new.to_i + response['expires_in'] + begin + raw_response = ssl_post(auth_url(), login_info.to_json, { + 'content-Type' => 'application/json' + }) + rescue ResponseError => e + raise OAuthResponseError.new(e) + else + response = parse(raw_response) + @access_token[:access_token] = response['access_token'] + @access_token[:expires_at] = Time.new.to_i + response['expires_in'] + end end end end 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..d8180010709 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) @@ -257,8 +261,7 @@ def post_data(action, parameters = {}) post[:password] = @options[:password] post[:type] = action if action - request = post.merge(parameters).map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') - request + post.merge(parameters).map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end def determine_funding_source(source) 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/spreedly_core.rb b/lib/active_merchant/billing/gateways/spreedly_core.rb index a286892086f..1e2a4715a3c 100644 --- a/lib/active_merchant/billing/gateways/spreedly_core.rb +++ b/lib/active_merchant/billing/gateways/spreedly_core.rb @@ -271,11 +271,9 @@ def childnode_to_response(response, node, childnode) end end - def build_xml_request(root) + def build_xml_request(root, &block) builder = Nokogiri::XML::Builder.new - builder.__send__(root) do |doc| - yield(doc) - end + builder.__send__(root, &block) builder.to_xml end diff --git a/lib/active_merchant/billing/gateways/stripe.rb b/lib/active_merchant/billing/gateways/stripe.rb index e85d0ece147..17bc8c5035c 100644 --- a/lib/active_merchant/billing/gateways/stripe.rb +++ b/lib/active_merchant/billing/gateways/stripe.rb @@ -7,14 +7,16 @@ module Billing #:nodoc: class StripeGateway < Gateway self.live_url = 'https://api.stripe.com/v1/' + # Docs on AVS codes: https://en.wikipedia.org/w/index.php?title=Address_verification_service&_ga=2.97570079.1027215965.1655989706-2008268124.1655989706#AVS_response_codes + # possible response values: https://stripe.com/docs/api/payment_methods/object#payment_method_object-card-checks AVS_CODE_TRANSLATOR = { - 'line1: pass, zip: pass' => 'Y', 'line1: pass, zip: fail' => 'A', 'line1: pass, zip: unchecked' => 'B', - 'line1: fail, zip: pass' => 'Z', + 'line1: unchecked, zip: unchecked' => 'I', 'line1: fail, zip: fail' => 'N', 'line1: unchecked, zip: pass' => 'P', - 'line1: unchecked, zip: unchecked' => 'I' + 'line1: pass, zip: pass' => 'Y', + 'line1: fail, zip: pass' => 'Z' } CVC_CODE_TRANSLATOR = { @@ -292,7 +294,9 @@ def scrub(transcript) gsub(%r(((\[card\]|card)\[number\]=)\d+), '\1[FILTERED]'). gsub(%r(((\[card\]|card)\[swipe_data\]=)[^&]+(&?)), '\1[FILTERED]\3'). gsub(%r(((\[bank_account\]|bank_account)\[account_number\]=)\d+), '\1[FILTERED]'). - gsub(%r(((\[payment_method_data\]|payment_method_data)\[card\]\[token\]=)[^&]+(&?)), '\1[FILTERED]\3') + gsub(%r(((\[payment_method_data\]|payment_method_data)\[card\]\[token\]=)[^&]+(&?)), '\1[FILTERED]\3'). + gsub(%r(((\[payment_method_data\]|payment_method_data)\[card\]\[network_token\]\[number\]=)\d+), '\1[FILTERED]'). + gsub(%r(((\[payment_method_options\]|payment_method_options)\[card\]\[network_token\]\[cryptogram\]=)[^&]+(&?)), '\1[FILTERED]') end def supports_network_tokenization? @@ -577,7 +581,7 @@ def add_shipping_address(post, payment, options = {}) def add_source_owner(post, creditcard, options) post[:owner] = {} - post[:owner][:name] = creditcard.name if creditcard.name + post[:owner][:name] = creditcard.name if creditcard.respond_to?(:name) && creditcard.name post[:owner][:email] = options[:email] if options[:email] if address = options[:billing_address] || options[:address] @@ -652,18 +656,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 @@ -694,31 +699,46 @@ 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) - 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, + card_checks = card_from_response(response) + avs_code = AVS_CODE_TRANSLATOR["line1: #{card_checks['address_line1_check']}, zip: #{card_checks['address_zip_check'] || card_checks['address_postal_code_check']}"] + cvc_code = CVC_CODE_TRANSLATOR[card_checks['cvc_check']] + Response.new( + success, message_from(success, response), response, test: response_is_test?(response), - authorization: authorization_from(success, url, method, response), + authorization: authorization_from(success, url, method, response, 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| + return false if key(options).start_with?(k) && !key(options).start_with?("#{k}_test") + end + + true end - def authorization_from(success, url, method, response) - return response.fetch('error', {})['charge'] unless success + def authorization_from(success, url, method, response, options) + return response.dig('error', 'charge') || response.dig('error', 'setup_intent', 'id') || response['id'] unless success if url == 'customers' [response['id'], response.dig('sources', 'data').first&.dig('id')].join('|') - elsif method == :post && (url.match(/customers\/.*\/cards/) || url.match(/payment_methods\/.*\/attach/)) - [response['customer'], response['id']].join('|') + elsif method == :post && (url.match(/customers\/.*\/cards/) || url.match(/payment_methods\/.*\/attach/) || options[:action] == :store) + response_id = options[:action] == :store ? response['payment_method'] : response['id'] + [response['customer'], response_id].join('|') else response['id'] end @@ -767,7 +787,10 @@ def quickchip_payment?(payment) end def card_from_response(response) - response['card'] || response['active_card'] || response['source'] || {} + # StripePI puts the AVS and CVC check significantly deeper into the response object + response['card'] || response['active_card'] || response['source'] || + response.dig('charges', 'data', 0, 'payment_method_details', 'card', 'checks') || + response.dig('latest_attempt', 'payment_method_details', 'card', 'checks') || {} end def emv_authorization_from_response(response) diff --git a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb index 7135cf7d2a0..4a9582aced1 100644 --- a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +++ b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb @@ -11,10 +11,15 @@ class StripePaymentIntentsGateway < StripeGateway CONFIRM_INTENT_ATTRIBUTES = %i[receipt_email return_url save_payment_method setup_future_usage off_session] UPDATE_INTENT_ATTRIBUTES = %i[description statement_descriptor_suffix statement_descriptor receipt_email setup_future_usage] DEFAULT_API_VERSION = '2020-08-27' + DIGITAL_WALLETS = { + apple_pay: 'apple_pay', + google_pay: 'google_pay_dpan', + untokenized_google_pay: 'google_pay_ecommerce_token' + } 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) && options[:new_ap_gp_route] != true r.process { tokenize_apple_google(payment_method, options) } payment_method = (r.params['token']['id']) if r.success? end @@ -25,24 +30,32 @@ def create_intent(money, payment_method, options = {}) add_confirmation_method(post, options) add_customer(post, options) - result = add_payment_method_token(post, payment_method, options) - return result if result.is_a?(ActiveMerchant::Billing::Response) + if new_apple_google_pay_flow(payment_method, options) + add_digital_wallet(post, payment_method, options) + add_billing_address(post, payment_method, options) + else + result = add_payment_method_token(post, payment_method, options) + return result if result.is_a?(ActiveMerchant::Billing::Response) + end + 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) + add_card_brand(post, options) + post[:expand] = ['charges.data.balance_transaction'] CREATE_INTENT_ATTRIBUTES.each do |attribute| add_whitelisted_attribute(post, options, attribute) @@ -56,10 +69,19 @@ 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) - return result if result.is_a?(ActiveMerchant::Billing::Response) + if new_apple_google_pay_flow(payment_method, options) + add_digital_wallet(post, payment_method, options) + else + result = add_payment_method_token(post, payment_method, options) + return result if result.is_a?(ActiveMerchant::Billing::Response) + end add_payment_method_types(post, options) CONFIRM_INTENT_ATTRIBUTES.each do |attribute| @@ -71,22 +93,33 @@ 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 new_apple_google_pay_flow(payment_method, options) + return false unless options[:new_ap_gp_route] + + payment_method.is_a?(NetworkTokenizationCreditCard) && digital_wallet_payment_method?(payment_method) + 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) @@ -100,8 +133,12 @@ def update_intent(money, intent_id, payment_method, options = {}) post = {} add_amount(post, money, options) - result = add_payment_method_token(post, payment_method, options) - return result if result.is_a?(ActiveMerchant::Billing::Response) + if new_apple_google_pay_flow(payment_method, options) + add_digital_wallet(post, payment_method, options) + else + result = add_payment_method_token(post, payment_method, options) + return result if result.is_a?(ActiveMerchant::Billing::Response) + end add_payment_method_types(post, options) add_customer(post, options) @@ -117,20 +154,32 @@ 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) - 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] + if new_apple_google_pay_flow(payment_method, options) + add_digital_wallet(post, payment_method, options) + add_billing_address(post, payment_method, options) + else + result = add_payment_method_token(post, payment_method, options, r) + return result if result.is_a?(ActiveMerchant::Billing::Response) + end - commit(:post, 'setup_intents', post, options) + add_metadata(post, options) + add_return_url(post, options) + add_fulfillment_date(post, options) + request_three_d_secure(post, options) + add_card_brand(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] + post[:expand] = ['latest_attempt'] + + commit(:post, 'setup_intents', post, options) + end + end end def retrieve_setup_intent(setup_intent_id, options = {}) @@ -196,23 +245,16 @@ def refund(money, intent_id, options = {}) # All other types will default to legacy Stripe store def store(payment_method, options = {}) params = {} - 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) + if new_apple_google_pay_flow(payment_method, options) + options[:customer] = customer(payment_method, options).params['id'] unless options[:customer] + verify(payment_method, options.merge!(action: :store)) + elsif 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(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? @@ -222,6 +264,24 @@ def store(payment_method, options = {}) end end + def customer(payment, options) + post = {} + 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('|') @@ -232,7 +292,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 = {}) @@ -245,8 +305,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 @@ -285,6 +359,14 @@ def add_metadata(post, options = {}) post[:metadata][:event_type] = options[:event_type] if options[:event_type] end + def add_card_brand(post, options) + return unless options[:card_brand] + + post[:payment_method_options] ||= {} + post[:payment_method_options][:card] ||= {} + post[:payment_method_options][:card][:network] = options[:card_brand] if options[:card_brand] + end + def add_level_three(post, options = {}) level_three = {} @@ -305,7 +387,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] = { @@ -318,7 +400,75 @@ 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 add_digital_wallet(post, payment_method, options) + post[:payment_method_data] = { + type: 'card', + card: { + last4: options[:last_4] || payment_method.number[-4..], + exp_month: payment_method.month, + exp_year: payment_method.year, + network_token: { + number: payment_method.number, + exp_month: payment_method.month, + exp_year: payment_method.year + } + } + } + + add_cryptogram_and_eci(post, payment_method, options) unless options[:wallet_type] + source = payment_method.respond_to?(:source) ? payment_method.source : options[:wallet_type] + post[:payment_method_data][:card][:network_token][:tokenization_method] = DIGITAL_WALLETS[source] + end + + def add_cryptogram_and_eci(post, payment_method, options) + 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.respond_to?(:payment_cryptogram) ? payment_method.payment_cryptogram : options[:cryptogram], + electronic_commerce_indicator: format_eci(payment_method, options) + }.compact + end + + def format_eci(payment_method, options) + eci_value = payment_method.respond_to?(:eci) ? payment_method.eci : options[:eci] + + if eci_value&.length == 1 + "0#{eci_value}" + else + eci_value end end @@ -347,6 +497,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'] @@ -358,16 +509,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 @@ -386,24 +538,78 @@ 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] + card_options[:mit_exemption][:network_transaction_id] = stored_credential[:network_transaction_id] if !(options[:setup_future_usage] == 'off_session') && (stored_credential[:network_transaction_id]) + + 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 = {}) @@ -413,7 +619,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 = {}) @@ -429,6 +635,16 @@ def add_claim_without_transaction_id(post, options = {}) post[:payment_method_options][:card][:mit_exemption][:claim_without_transaction_id] = options[:claim_without_transaction_id] end + def add_billing_address_for_card_tokenization(post, options = {}) + return unless (billing = options[:billing_address] || options[:address]) + + billing = add_address(billing, options) + billing[:address].transform_keys! { |k| k == :postal_code ? :address_zip : k.to_s.prepend('address_').to_sym } + + post[:card][:name] = billing[:name] + post[:card].merge!(billing[:address]) + end + def add_error_on_requires_action(post, options = {}) return unless options[:confirm] @@ -462,52 +678,51 @@ def setup_future_usage(post, options = {}) post end - def add_billing_address(post, options = {}) - return unless billing = options[:billing_address] || options[:address] - - email = billing[:email] || options[:email] - - 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] = email if email - post[:billing_details][:name] = billing[:name] if billing[:name] - post[:billing_details][:phone] = billing[:phone] if billing[:phone] - end + def add_billing_address(post, payment_method, options = {}) + return if payment_method.nil? || payment_method.is_a?(StripePaymentToken) || payment_method.is_a?(String) - def add_name_only(post, payment_method) - post[:billing_details] = {} unless post[:billing_details] + post[:payment_method_data] ||= {} + if billing = options[:billing_address] || options[:address] + post[:payment_method_data][:billing_details] = add_address(billing, options) + end - name = [payment_method.first_name, payment_method.last_name].compact.join(' ') - post[:billing_details][:name] = name + unless post[:payment_method_data][:billing_details] + name = [payment_method.first_name, payment_method.last_name].compact.join(' ') + post[:payment_method_data][:billing_details] = { name: name } + end 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] 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..85018165efb --- /dev/null +++ b/lib/active_merchant/billing/gateways/sum_up.rb @@ -0,0 +1,223 @@ +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 refund(money, authorization, options = {}) + transaction_id = authorization.split('#').last + post = money ? { amount: amount(money) } : {} + 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) + add_3ds_data(post, 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) + post[:checkout_reference] = options[:order_id] + post[:amount] = amount(money) + post[:currency] = options[:currency] || currency(money) + 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 add_3ds_data(post, options) + post[:redirect_url] = options[:redirect_url] if options[:redirect_url] + end + + def commit(action, post, method = :post) + response = api_request(action, post.compact, method) + succeeded = success_from(response) + + Response.new( + succeeded, + message_from(succeeded, response), + action.include?('refund') ? { response_code: response.to_s } : response, + authorization: authorization_from(response), + test: test?, + error_code: error_code_from(succeeded, response) + ) + end + + def api_request(action, post, method) + raw_response = + begin + ssl_request(method, live_url + action, post.to_json, auth_headers) + rescue ResponseError => e + e.response.body + end + response = parse(raw_response) + response = response.is_a?(Hash) ? response.symbolize_keys : response + + return format_errors(response) if raw_response.include?('error_code') && response.is_a?(Array) + + response + end + + def parse(body) + JSON.parse(body) + end + + def success_from(response) + (response.is_a?(Hash) && response[:next_step]) || + response == 204 || + %w(PENDING PAID).include?(response[:status]) || + response[:transactions]&.all? { |transaction| transaction.symbolize_keys[:status] == 'SUCCESSFUL' } + end + + def message_from(succeeded, response) + if succeeded + return 'Succeeded' if (response.is_a?(Hash) && response[:next_step]) || response == 204 + + return response[:status] + end + + response[:message] || response[:error_message] || response[:status] + end + + def authorization_from(response) + return nil if response.is_a?(Integer) + + 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(succeeded, response) + response[:error_code] unless succeeded + end + + def format_error(error, key) + { + :error_code => error['error_code'], + key => error['param'] + } + end + + def format_errors(errors) + return format_error(errors.first, :message) if errors.size == 1 + + return { + error_code: STANDARD_ERROR_CODE_MAPPING[:multiple_invalid_parameters], + message: 'Validation error', + errors: errors.map { |error| format_error(error, :param) } + } + end + + def handle_response(response) + case response.code.to_i + # to get the response code (204) when 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/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/telr.rb b/lib/active_merchant/billing/gateways/telr.rb index 76c47c1dba4..620ea242f54 100644 --- a/lib/active_merchant/billing/gateways/telr.rb +++ b/lib/active_merchant/billing/gateways/telr.rb @@ -162,9 +162,9 @@ def lookup_country_code(code) country.code(:alpha2) end - def commit(action, amount = nil, currency = nil) + def commit(action, amount = nil, currency = nil, &block) currency = default_currency if currency == nil - request = build_xml_request { |doc| yield(doc) } + request = build_xml_request(&block) response = ssl_post(live_url, request, headers) parsed = parse(response) @@ -231,8 +231,7 @@ def parse(xml) def authorization_from(action, response, amount, currency) auth = response[:tranref] - auth = [auth, amount, currency].join('|') - auth + [auth, amount, currency].join('|') end def split_authorization(authorization) 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.rb b/lib/active_merchant/billing/gateways/trans_first.rb index 55215e8c0e0..6cf3756843e 100644 --- a/lib/active_merchant/billing/gateways/trans_first.rb +++ b/lib/active_merchant/billing/gateways/trans_first.rb @@ -219,8 +219,7 @@ def post_data(action, params = {}) params[:MerchantID] = @options[:login] params[:RegKey] = @options[:password] - request = params.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') - request + params.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&') end def add_pair(post, key, value, options = {}) 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 36a5d43084d..b9f3b237277 100644 --- a/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb +++ b/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb @@ -438,29 +438,21 @@ def refund_type(action) end # -- request methods --------------------------------------------------- - def build_xml_transaction_request - build_xml_request('SendTranRequest') do |doc| - yield doc - end + def build_xml_transaction_request(&block) + build_xml_request('SendTranRequest', &block) end - def build_xml_payment_storage_request - build_xml_request('UpdtRecurrProfRequest') do |doc| - yield doc - end + def build_xml_payment_storage_request(&block) + build_xml_request('UpdtRecurrProfRequest', &block) end - def build_xml_payment_update_request + def build_xml_payment_update_request(&block) merchant_product_type = 5 # credit card - build_xml_request('UpdtRecurrProfRequest', merchant_product_type) do |doc| - yield doc - end + build_xml_request('UpdtRecurrProfRequest', merchant_product_type, &block) end - def build_xml_payment_search_request - build_xml_request('FndRecurrProfRequest') do |doc| - yield doc - end + def build_xml_payment_search_request(&block) + build_xml_request('FndRecurrProfRequest', &block) end def build_xml_request(wrapper, merchant_product_type = nil) diff --git a/lib/active_merchant/billing/gateways/transact_pro.rb b/lib/active_merchant/billing/gateways/transact_pro.rb index bda2602c49d..4f017bd525e 100644 --- a/lib/active_merchant/billing/gateways/transact_pro.rb +++ b/lib/active_merchant/billing/gateways/transact_pro.rb @@ -172,7 +172,7 @@ def parse(body) { status: 'success', id: m[2] } : { status: 'failure', message: m[2] } else - Hash[status: body] + { status: body } end 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 f909e84f55d..09d0bbe9519 100644 --- a/lib/active_merchant/billing/gateways/vanco.rb +++ b/lib/active_merchant/billing/gateways/vanco.rb @@ -281,11 +281,9 @@ def login_request end end - def build_xml_request + def build_xml_request(&block) builder = Nokogiri::XML::Builder.new - builder.__send__('VancoWS') do |doc| - yield(doc) - end + builder.__send__('VancoWS', &block) builder.to_xml end diff --git a/lib/active_merchant/billing/gateways/vantiv_express.rb b/lib/active_merchant/billing/gateways/vantiv_express.rb new file mode 100644 index 00000000000..4bbf3160414 --- /dev/null +++ b/lib/active_merchant/billing/gateways/vantiv_express.rb @@ -0,0 +1,587 @@ +require 'nokogiri' +require 'securerandom' + +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class VantivExpressGateway < Gateway + self.test_url = 'https://certtransaction.elementexpress.com' + self.live_url = 'https://transaction.elementexpress.com' + + self.supported_countries = ['US'] + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb] + + self.homepage_url = 'http://www.elementps.com' + self.display_name = 'Element' + + SERVICE_TEST_URL = 'https://certservices.elementexpress.com' + SERVICE_LIVE_URL = 'https://services.elementexpress.com' + + NETWORK_TOKEN_TYPE = { + apple_pay: 2, + google_pay: 1 + } + + CARD_PRESENT_CODE = { + 'Unknown' => 1, + 'Present' => 2, + 'NotPresent' => 3 + } + + MARKET_CODE = { + 'AutoRental' => 1, + 'DirectMarketing' => 2, + 'ECommerce' => 3, + 'FoodRestaurant' => 4, + 'HotelLodging' => 5, + 'Petroleum' => 6, + 'Retail' => 7, + 'QSR' => 8, + 'Grocery' => 9 + } + + PAYMENT_TYPE = { + 'NotUsed' => 0, + 'Recurring' => 1, + 'Installment' => 2, + 'CardHolderInitiated' => 3, + 'CredentialOnFile' => 4 + } + + REVERSAL_TYPE = { + 'System' => 0, + 'Full' => 1, + 'Partial' => 2 + } + + SUBMISSION_TYPE = { + 'NotUsed' => 0, + 'Initial' => 1, + 'Subsequent' => 2, + 'Resubmission' => 3, + 'ReAuthorization' => 4, + 'DelayedCharges' => 5, + 'NoShow' => 6 + } + + LODGING_PPC = { + 'NonParticipant' => 0, + 'DollarLimit500' => 1, + 'DollarLimit1000' => 2, + 'DollarLimit1500' => 3 + } + + LODGING_SPC = { + 'Default' => 0, + 'Sale' => 1, + 'NoShow' => 2, + 'AdvanceDeposit' => 3 + } + + LODGING_CHARGE_TYPE = { + 'Default' => 0, + 'Restaurant' => 1, + 'GiftShop' => 2 + } + + TERMINAL_TYPE = { + 'Unknown' => 0, + 'PointOfSale' => 1, + 'ECommerce' => 2, + 'MOTO' => 3, + 'FuelPump' => 4, + 'ATM' => 5, + 'Voice' => 6, + 'Mobile' => 7, + 'WebSiteGiftCard' => 8 + } + + CARD_HOLDER_PRESENT_CODE = { + 'Default' => 0, + 'Unknown' => 1, + 'Present' => 2, + 'NotPresent' => 3, + 'MailOrder' => 4, + 'PhoneOrder' => 5, + 'StandingAuth' => 6, + 'ECommerce' => 7 + } + + CARD_INPUT_CODE = { + 'Default' => 0, + 'Unknown' => 1, + 'MagstripeRead' => 2, + 'ContactlessMagstripeRead' => 3, + 'ManualKeyed' => 4, + 'ManualKeyedMagstripeFailure' => 5, + 'ChipRead' => 6, + 'ContactlessChipRead' => 7, + 'ManualKeyedChipReadFailure' => 8, + 'MagstripeReadChipReadFailure' => 9, + 'MagstripeReadNonTechnicalFallback' => 10 + } + + CVV_PRESENCE_CODE = { + 'UseDefault' => 0, + 'NotProvided' => 1, + 'Provided' => 2, + 'Illegible' => 3, + 'CustomerIllegible' => 4 + } + + TERMINAL_CAPABILITY_CODE = { + 'Default' => 0, + 'Unknown' => 1, + 'NoTerminal' => 2, + 'MagstripeReader' => 3, + 'ContactlessMagstripeReader' => 4, + 'KeyEntered' => 5, + 'ChipReader' => 6, + 'ContactlessChipReader' => 7 + } + + TERMINAL_ENVIRONMENT_CODE = { + 'Default' => 0, + 'NoTerminal' => 1, + 'LocalAttended' => 2, + 'LocalUnattended' => 3, + 'RemoteAttended' => 4, + 'RemoteUnattended' => 5, + 'ECommerce' => 6 + } + + def initialize(options = {}) + requires!(options, :account_id, :account_token, :application_id, :acceptor_id, :application_name, :application_version) + super + end + + def purchase(money, payment, options = {}) + action = payment.is_a?(Check) ? 'CheckSale' : 'CreditCardSale' + eci = parse_eci(payment) + + request = build_xml_request do |xml| + xml.send(action, xmlns: live_url) do + add_credentials(xml) + add_payment_method(xml, payment) + add_transaction(xml, money, options, eci) + add_terminal(xml, options, eci) + add_address(xml, options) + add_lodging(xml, options) + end + end + + commit(request, money, payment) + end + + def authorize(money, payment, options = {}) + eci = parse_eci(payment) + + request = build_xml_request do |xml| + xml.CreditCardAuthorization(xmlns: live_url) do + add_credentials(xml) + add_payment_method(xml, payment) + add_transaction(xml, money, options, eci) + add_terminal(xml, options, eci) + add_address(xml, options) + add_lodging(xml, options) + end + end + + commit(request, money, payment) + end + + def capture(money, authorization, options = {}) + trans_id, _, eci = authorization.split('|') + options[:trans_id] = trans_id + + request = build_xml_request do |xml| + xml.CreditCardAuthorizationCompletion(xmlns: live_url) do + add_credentials(xml) + add_transaction(xml, money, options, eci) + add_terminal(xml, options, eci) + end + end + + commit(request, money) + end + + def refund(money, authorization, options = {}) + trans_id, _, eci = authorization.split('|') + options[:trans_id] = trans_id + + request = build_xml_request do |xml| + xml.CreditCardReturn(xmlns: live_url) do + add_credentials(xml) + add_transaction(xml, money, options, eci) + add_terminal(xml, options, eci) + end + end + + commit(request, money) + end + + def credit(money, payment, options = {}) + eci = parse_eci(payment) + + request = build_xml_request do |xml| + xml.CreditCardCredit(xmlns: live_url) do + add_credentials(xml) + add_payment_method(xml, payment) + add_transaction(xml, money, options, eci) + add_terminal(xml, options, eci) + end + end + + commit(request, money, payment) + end + + def void(authorization, options = {}) + trans_id, trans_amount, eci = authorization.split('|') + options.merge!({ trans_id: trans_id, trans_amount: trans_amount, reversal_type: 1 }) + + request = build_xml_request do |xml| + xml.CreditCardReversal(xmlns: live_url) do + add_credentials(xml) + add_transaction(xml, trans_amount, options, eci) + add_terminal(xml, options, eci) + end + end + + commit(request, trans_amount) + end + + def store(payment, options = {}) + request = build_xml_request do |xml| + xml.PaymentAccountCreate(xmlns: SERVICE_LIVE_URL) do + add_credentials(xml) + add_payment_method(xml, payment) + add_payment_account(xml, payment, options[:payment_account_reference_number] || SecureRandom.hex(20)) + add_address(xml, options) + end + end + + commit(request, payment, nil, :store) + end + + def verify(payment, options = {}) + eci = parse_eci(payment) + + request = build_xml_request do |xml| + xml.CreditCardAVSOnly(xmlns: live_url) do + add_credentials(xml) + add_payment_method(xml, payment) + add_transaction(xml, 0, options, eci) + add_terminal(xml, options, eci) + add_address(xml, options) + end + end + + commit(request) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r(().+?())i, '\1[FILTERED]\2'). + gsub(%r(().+?())i, '\1[FILTERED]\2'). + gsub(%r(().+?())i, '\1[FILTERED]\2'). + gsub(%r(().+?())i, '\1[FILTERED]\2'). + gsub(%r(().+?())i, '\1[FILTERED]\2') + end + + private + + def add_credentials(xml) + xml.Credentials do + xml.AccountID @options[:account_id] + xml.AccountToken @options[:account_token] + xml.AcceptorID @options[:acceptor_id] + end + xml.Application do + xml.ApplicationID @options[:application_id] + xml.ApplicationName @options[:application_name] + xml.ApplicationVersion @options[:application_version] + end + end + + def add_payment_method(xml, payment) + if payment.is_a?(String) + 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 + end + + def add_payment_account(xml, payment, payment_account_reference_number) + xml.PaymentAccount do + xml.PaymentAccountType payment_account_type(payment) + xml.PaymentAccountReferenceNumber payment_account_reference_number + end + end + + def add_payment_account_id(xml, payment) + xml.PaymentAccount do + xml.PaymentAccountID payment + end + end + + def add_transaction(xml, money, options = {}, network_token_eci = nil) + xml.Transaction do + xml.ReversalType REVERSAL_TYPE[options[:reversal_type]] || options[:reversal_type] if options[:reversal_type] + xml.TransactionID options[:trans_id] if options[:trans_id] + xml.TransactionAmount amount(money.to_i) if money + xml.MarketCode market_code(money, options, network_token_eci) if options[:market_code] || money + xml.ReferenceNumber options[:order_id].present? ? options[:order_id][0, 50] : SecureRandom.hex(20) + xml.TicketNumber options[:ticket_number] || rand(1..999999) + xml.MerchantSuppliedTransactionID options[:merchant_supplied_transaction_id] if options[:merchant_supplied_transaction_id] + xml.PaymentType PAYMENT_TYPE[options[:payment_type]] || options[:payment_type] if options[:payment_type] + xml.SubmissionType SUBMISSION_TYPE[options[:submission_type]] || options[:submission_type] if options[:submission_type] + xml.DuplicateCheckDisableFlag 1 if options[:duplicate_check_disable_flag].to_s == 'true' || options[:duplicate_override_flag].to_s == 'true' + end + end + + def parse_eci(payment) + return nil unless payment.is_a?(NetworkTokenizationCreditCard) + + if (eci = payment.eci) + eci = eci[0] == '0' ? eci.sub!(/^0/, '') : eci + return eci + else + payment.brand == 'american_express' ? '9' : '6' + end + end + + def market_code(money, options, network_token_eci) + return 3 if network_token_eci + + MARKET_CODE[options[:market_code]] || options[:market_code] || 0 + end + + def add_lodging(xml, options) + if options[:lodging] + lodging = parse_lodging(options[:lodging]) + xml.ExtendedParameters do + xml.Lodging do + xml.LodgingAgreementNumber lodging[:agreement_number] if lodging[:agreement_number] + xml.LodgingCheckInDate lodging[:check_in_date] if lodging[:check_in_date] + xml.LodgingCheckOutDate lodging[:check_out_date] if lodging[:check_out_date] + xml.LodgingRoomAmount lodging[:room_amount] if lodging[:room_amount] + xml.LodgingRoomTax lodging[:room_tax] if lodging[:room_tax] + xml.LodgingNoShowIndicator lodging[:no_show_indicator] if lodging[:no_show_indicator] + xml.LodgingDuration lodging[:duration] if lodging[:duration] + xml.LodgingCustomerName lodging[:customer_name] if lodging[:customer_name] + xml.LodgingClientCode lodging[:client_code] if lodging[:client_code] + xml.LodgingExtraChargesDetail lodging[:extra_charges_detail] if lodging[:extra_charges_detail] + xml.LodgingExtraChargesAmounts lodging[:extra_charges_amounts] if lodging[:extra_charges_amounts] + xml.LodgingPrestigiousPropertyCode lodging[:prestigious_property_code] if lodging[:prestigious_property_code] + xml.LodgingSpecialProgramCode lodging[:special_program_code] if lodging[:special_program_code] + xml.LodgingChargeType lodging[:charge_type] if lodging[:charge_type] + end + end + end + end + + def add_terminal(xml, options, network_token_eci = nil) + options = parse_terminal(options) + + xml.Terminal do + xml.TerminalID options[:terminal_id] || '01' + xml.TerminalType options[:terminal_type] if options[:terminal_type] + xml.CardPresentCode options[:card_present_code] || 0 + xml.CardholderPresentCode options[:card_holder_present_code] || 0 + xml.CardInputCode options[:card_input_code] || 0 + xml.CVVPresenceCode options[:cvv_presence_code] || 0 + xml.TerminalCapabilityCode options[:terminal_capability_code] || 0 + xml.TerminalEnvironmentCode options[:terminal_environment_code] || 0 + xml.MotoECICode network_token_eci || 7 + xml.PartialApprovedFlag options[:partial_approved_flag] if options[:partial_approved_flag] + end + end + + def add_credit_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.CVV payment.verification_value + end + end + + def add_echeck(xml, payment) + xml.DemandDepositAccount do + xml.AccountNumber payment.account_number + xml.RoutingNumber payment.routing_number + xml.DDAAccountType payment.account_type == 'checking' ? 0 : 1 + 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.WalletType NETWORK_TOKEN_TYPE[payment.source] + end + end + + def add_address(xml, options) + address = address = options[:billing_address] || options[:address] + shipping_address = options[:shipping_address] + + if address || shipping_address + xml.Address do + if address + address[:email] ||= options[:email] + + xml.BillingAddress1 address[:address1] if address[:address1] + xml.BillingAddress2 address[:address2] if address[:address2] + xml.BillingCity address[:city] if address[:city] + xml.BillingState address[:state] if address[:state] + xml.BillingZipcode address[:zip] if address[:zip] + xml.BillingEmail address[:email] if address[:email] + xml.BillingPhone address[:phone_number] if address[:phone_number] + end + + if shipping_address + xml.ShippingAddress1 shipping_address[:address1] if shipping_address[:address1] + xml.ShippingAddress2 shipping_address[:address2] if shipping_address[:address2] + xml.ShippingCity shipping_address[:city] if shipping_address[:city] + xml.ShippingState shipping_address[:state] if shipping_address[:state] + xml.ShippingZipcode shipping_address[:zip] if shipping_address[:zip] + xml.ShippingEmail shipping_address[:email] if shipping_address[:email] + xml.ShippingPhone shipping_address[:phone_number] if shipping_address[:phone_number] + end + end + end + end + + def parse(xml) + response = {} + + doc = Nokogiri::XML(xml) + doc.remove_namespaces! + root = doc.root.xpath('//response/*') + + root = doc.root.xpath('//Response/*') if root.empty? + + root.each do |node| + if node.elements.empty? + response[node.name.downcase] = node.text + else + node_name = node.name.downcase + response[node_name] = {} + + node.elements.each do |childnode| + response[node_name][childnode.name.downcase] = childnode.text + end + end + end + + response + end + + def parse_lodging(lodging) + lodging[:prestigious_property_code] = LODGING_PPC[lodging[:prestigious_property_code]] || lodging[:prestigious_property_code] if lodging[:prestigious_property_code] + lodging[:special_program_code] = LODGING_SPC[lodging[:special_program_code]] || lodging[:special_program_code] if lodging[:special_program_code] + lodging[:charge_type] = LODGING_CHARGE_TYPE[lodging[:charge_type]] || lodging[:charge_type] if lodging[:charge_type] + + lodging + end + + def parse_terminal(options) + options[:terminal_type] = TERMINAL_TYPE[options[:terminal_type]] || options[:terminal_type] + options[:card_present_code] = CARD_PRESENT_CODE[options[:card_present_code]] || options[:card_present_code] + options[:card_holder_present_code] = CARD_HOLDER_PRESENT_CODE[options[:card_holder_present_code]] || options[:card_holder_present_code] + options[:card_input_code] = CARD_INPUT_CODE[options[:card_input_code]] || options[:card_input_code] + options[:cvv_presence_code] = CVV_PRESENCE_CODE[options[:cvv_presence_code]] || options[:cvv_presence_code] + options[:terminal_capability_code] = TERMINAL_CAPABILITY_CODE[options[:terminal_capability_code]] || options[:terminal_capability_code] + options[:terminal_environment_code] = TERMINAL_ENVIRONMENT_CODE[options[:terminal_environment_code]] || options[:terminal_environment_code] + + options + end + + def commit(xml, amount = nil, payment = nil, action = nil) + response = parse(ssl_post(url(action), xml, headers)) + success = success_from(response) + + Response.new( + success, + message_from(response), + response, + authorization: authorization_from(action, response, amount, payment), + avs_result: success ? avs_from(response) : nil, + cvv_result: success ? cvv_from(response) : nil, + test: test? + ) + end + + def authorization_from(action, response, amount, payment) + return response.dig('paymentaccount', 'paymentaccountid') if action == :store + + if response['transaction'] + authorization = "#{response.dig('transaction', 'transactionid')}|#{amount}" + authorization << "|#{parse_eci(payment)}" if parse_eci(payment) + authorization + end + end + + def success_from(response) + response['expressresponsecode'] == '0' + end + + def message_from(response) + response['expressresponsemessage'] + end + + def avs_from(response) + AVSResult.new(code: response['card']['avsresponsecode']) if response['card'] + end + + def cvv_from(response) + CVVResult.new(response['card']['cvvresponsecode']) if response['card'] + end + + def build_xml_request(&block) + builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8', &block) + + builder.to_xml + end + + def payment_account_type(payment) + return 0 unless payment.is_a?(Check) + + if payment.account_type == 'checking' + 1 + elsif payment.account_type == 'savings' + 2 + else + 3 + end + end + + def url(action) + if action == :store + test? ? SERVICE_TEST_URL : SERVICE_LIVE_URL + else + test? ? test_url : live_url + end + end + + def headers + { + 'Content-Type' => 'text/xml' + } + end + 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 25889cccd00..5c98fadfb2f 100644 --- a/lib/active_merchant/billing/gateways/visanet_peru.rb +++ b/lib/active_merchant/billing/gateways/visanet_peru.rb @@ -143,12 +143,12 @@ def split_authorization(authorization) end def generate_purchase_number_stamp - Time.now.to_f.to_s.delete('.')[1..10] + rand(99).to_s + rand(('9' * 12).to_i).to_s.center(12, rand(9).to_s) end def commit(action, params, options = {}) raw_response = ssl_request(method(action), url(action, params, options), params.to_json, headers) - response = parse(raw_response) + response = parse(raw_response).merge('purchaseNumber' => params[:purchaseNumber]) rescue ResponseError => e raw_response = e.response.body response_error(raw_response, options, action) diff --git a/lib/active_merchant/billing/gateways/vpos.rb b/lib/active_merchant/billing/gateways/vpos.rb index be259e3b7ca..3389637e965 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' @@ -137,7 +137,7 @@ def add_card_data(post, payment) card_number = payment.number cvv = payment.verification_value - payload = { card_number: card_number, 'cvv': cvv }.to_json + payload = { card_number: card_number, cvv: cvv }.to_json encryption_key = @encryption_key || OpenSSL::PKey::RSA.new(one_time_public_key) @@ -158,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 ed0f4536039..ff145e2a22d 100644 --- a/lib/active_merchant/billing/gateways/wompi.rb +++ b/lib/active_merchant/billing/gateways/wompi.rb @@ -34,6 +34,7 @@ def purchase(money, payment, options = {}) public_key: public_key } add_invoice(post, money, options) + add_tip_in_cents(post, options) add_card(post, payment, options) commit('sale', post, '/transactions_sync') @@ -141,6 +142,10 @@ def add_basic_card_info(post, card, options) post[:cvc] = cvc if cvc && !cvc.empty? end + def add_tip_in_cents(post, options) + post[:tip_in_cents] = options[:tip_in_cents].to_i if options[:tip_in_cents] + end + def parse(body) JSON.parse(body) end diff --git a/lib/active_merchant/billing/gateways/worldpay.rb b/lib/active_merchant/billing/gateways/worldpay.rb index da5ac218014..1eac5a69b0d 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 = {}) @@ -77,7 +62,7 @@ def purchase(money, payment_method, options = {}) def authorize(money, payment_method, options = {}) requires!(options, :order_id) - payment_details = payment_details(payment_method) + payment_details = payment_details(payment_method, options) authorize_request(money, payment_method, payment_details.merge(options)) end @@ -123,7 +108,7 @@ def refund(money, authorization, options = {}) # and other transactions should be performed on a normal eCom-flagged # merchant ID. def credit(money, payment_method, options = {}) - payment_details = payment_details(payment_method) + payment_details = payment_details(payment_method, options) if options[:fast_fund_credit] fast_fund_credit_request(money, payment_method, payment_details.merge(credit: true, **options)) else @@ -168,6 +153,14 @@ def scrub(transcript) private + def eci_value(payment_method, options) + eci = payment_method.respond_to?(:eci) ? format(payment_method.eci, :two_digits) : '' + + return eci unless eci.empty? + + options[:use_default_eci] ? '07' : eci + end + def authorize_request(money, payment_method, options) commit('authorize', build_authorization_request(money, payment_method, options), 'AUTHORISED', 'CAPTURED', options) end @@ -267,9 +260,8 @@ 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', + tax_amount: 'salesTax', discount_amount: 'discountAmount', shipping_amount: 'shippingAmount', duty_amount: 'dutyAmount' @@ -277,53 +269,37 @@ def add_level_two_and_three_data(xml, amount, data) 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) + add_amount(xml, data[key].to_i, data) 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 + data[:line_items].each { |item| add_line_items_into_level_three_data(xml, item.symbolize_keys, data) } if data.include?(:line_items) end - def add_items_into_level_three_data(xml, item) + def add_line_items_into_level_three_data(xml, item, data) 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 + xml.unitCost do + add_amount(xml, item[:unit_cost], data) + end + xml.unitOfMeasure item[:unit_of_measure] || 'each' + xml.itemTotal do + sub_total_amount = item[:quantity].to_i * (item[:unit_cost].to_i - item[:discount_amount].to_i) + add_amount(xml, sub_total_amount, data) + end + xml.itemTotalWithTax do + add_amount(xml, item[:total_amount], data) + end + xml.itemDiscountAmount do + add_amount(xml, item[:discount_amount], data) + end + xml.taxAmount do + add_amount(xml, item[:tax_amount], data) end end end @@ -333,7 +309,7 @@ def add_optional_data_level_two_and_three(xml, data) 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) + xml.taxExempt data[:tax_amount].to_i > 0 ? 'false' : 'true' end def order_tag_attributes(options) @@ -458,7 +434,7 @@ def add_token_for_ff_credit(xml, payment_method, options) end def add_additional_3ds_data(xml, options) - additional_data = { 'dfReferenceId' => options[:session_id] } + additional_data = { 'dfReferenceId' => options[:df_reference_id] } additional_data['challengeWindowSize'] = options[:browser_size] if options[:browser_size] xml.additional3DSData additional_data @@ -569,10 +545,10 @@ def add_date_element(xml, name, date) end def add_amount(xml, money, options) - currency = options[:currency] || currency(money) + currency = options[:currency] || currency(money.to_i) amount_hash = { - :value => localized_amount(money, currency), + :value => localized_amount(money.to_i, currency), 'currencyCode' => currency, 'exponent' => currency_exponent(currency) } @@ -587,7 +563,7 @@ def add_payment_method(xml, amount, payment_method, options) when :pay_as_order add_amount_for_pay_as_order(xml, amount, payment_method, options) when :network_token - add_network_tokenization_card(xml, payment_method) + add_network_tokenization_card(xml, payment_method, options) else add_card_or_token(xml, payment_method, options) end @@ -605,8 +581,9 @@ def add_amount_for_pay_as_order(xml, amount, payment_method, options) end end - def add_network_tokenization_card(xml, payment_method) - token_type = NETWORK_TOKEN_TYPE.fetch(payment_method.source, 'NETWORKTOKEN') + def add_network_tokenization_card(xml, payment_method, options) + source = payment_method.respond_to?(:source) ? payment_method.source : options[:wallet_type] + token_type = NETWORK_TOKEN_TYPE.fetch(source, 'NETWORKTOKEN') xml.paymentDetails do xml.tag! 'EMVCO_TOKEN-SSL', 'type' => token_type do @@ -618,11 +595,12 @@ def add_network_tokenization_card(xml, payment_method) ) end name = card_holder_name(payment_method, options) - eci = format(payment_method.eci, :two_digits) xml.cardHolderName name if name.present? - xml.cryptogram payment_method.payment_cryptogram - xml.eciIndicator eci.empty? ? '07' : eci + xml.cryptogram payment_method.payment_cryptogram unless options[:wallet_type] == :google_pay + eci = eci_value(payment_method, options) + xml.eciIndicator eci if eci.present? end + add_stored_credential_options(xml, options) end end @@ -646,7 +624,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 @@ -683,7 +661,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) @@ -698,30 +677,27 @@ def add_stored_credential_options(xml, options = {}) end def add_stored_credential_using_normalized_fields(xml, options) - if options[:stored_credential][:initial_transaction] - xml.storedCredentials 'usage' => 'FIRST' - else - reason = case options[:stored_credential][:reason_type] - when 'installment' then 'INSTALMENT' - when 'recurring' then 'RECURRING' - when 'unscheduled' then 'UNSCHEDULED' - end - - xml.storedCredentials 'usage' => 'USED', 'merchantInitiatedReason' => reason do - xml.schemeTransactionIdentifier options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id] - end + reason = case options[:stored_credential][:reason_type] + when 'installment' then 'INSTALMENT' + when 'recurring' then 'RECURRING' + when 'unscheduled' then 'UNSCHEDULED' + end + is_initial_transaction = options[:stored_credential][:initial_transaction] + stored_credential_params = generate_stored_credential_params(is_initial_transaction, reason) + + xml.storedCredentials stored_credential_params do + xml.schemeTransactionIdentifier network_transaction_id(options) if network_transaction_id(options) && !is_initial_transaction end end def add_stored_credential_using_gateway_specific_fields(xml, options) return unless options[:stored_credential_usage] - if options[:stored_credential_initiated_reason] - xml.storedCredentials 'usage' => options[:stored_credential_usage], 'merchantInitiatedReason' => options[:stored_credential_initiated_reason] do - xml.schemeTransactionIdentifier options[:stored_credential_transaction_id] if options[:stored_credential_transaction_id] - end - else - xml.storedCredentials 'usage' => options[:stored_credential_usage] + is_initial_transaction = options[:stored_credential_usage] == 'FIRST' + stored_credential_params = generate_stored_credential_params(is_initial_transaction, options[:stored_credential_initiated_reason]) + + xml.storedCredentials stored_credential_params do + xml.schemeTransactionIdentifier options[:stored_credential_transaction_id] if options[:stored_credential_transaction_id] && !is_initial_transaction end end @@ -846,7 +822,8 @@ def headers(options) # 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 = options[:cookie] || @cookie || nil + cookie = defined?(@cookie) ? @cookie : nil + cookie = options[:cookie] || cookie headers['Cookie'] = cookie if cookie headers['Idempotency-Key'] = idempotency_key if idempotency_key @@ -991,17 +968,23 @@ def token_details_from_authorization(authorization) token_details end - def payment_details(payment_method) + def payment_details(payment_method, options = {}) 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) || options[:wallet_type] == :google_pay ? :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 @@ -1027,10 +1010,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 @@ -1038,6 +1017,15 @@ def eligible_for_0_auth?(payment_method, options = {}) def card_holder_name(payment_method, options) test? && options[:execute_threed] && !options[:three_ds_version]&.start_with?('2') ? '3D' : payment_method.name end + + def generate_stored_credential_params(is_initial_transaction, reason = nil) + customer_or_merchant = reason == 'RECURRING' && is_initial_transaction ? 'customerInitiatedReason' : 'merchantInitiatedReason' + + stored_credential_params = {} + stored_credential_params['usage'] = is_initial_transaction ? 'FIRST' : 'USED' + stored_credential_params[customer_or_merchant] = reason if reason + stored_credential_params + end end end 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..260839af916 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 @@ -84,8 +86,7 @@ def create_token(reusable, name, exp_month, exp_year, number, cvc) }, 'clientKey' => @client_key } - token_response = commit(:post, 'tokens', obj, { 'Authorization' => @service_key }, 'token') - token_response + commit(:post, 'tokens', obj, { 'Authorization' => @service_key }, 'token') end def create_post_for_auth_or_purchase(token, money, options) @@ -134,7 +135,10 @@ def commit(method, url, parameters = nil, options = {}, type = false) raw_response = ssl_request(method, self.live_url + url, json, headers(options)) - if raw_response != '' + if raw_response == '' + success = true + response = {} + else response = parse(raw_response) if type == 'token' success = response.key?('token') @@ -151,9 +155,6 @@ def commit(method, url, parameters = nil, options = {}, type = false) end end end - else - success = true - response = {} end rescue ResponseError => e raw_response = e.response.body @@ -170,14 +171,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..66725fdc2b5 --- /dev/null +++ b/lib/active_merchant/billing/gateways/xpay.rb @@ -0,0 +1,242 @@ +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://xpaysandbox.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 = { + validation: 'orders/3steps/validation', + purchase: 'orders/3steps/payment', + authorize: 'orders/3steps/payment', + preauth: 'orders/3steps/init', + capture: 'operations/%s/captures', + verify: 'orders/card_verification', + refund: 'operations/%s/refunds' + } + + SUCCESS_MESSAGES = %w(PENDING AUTHORIZED THREEDS_VALIDATED EXECUTED).freeze + + def initialize(options = {}) + requires!(options, :api_key) + @api_key = options[:api_key] + super + end + + def preauth(amount, credit_card, options = {}) + order_request(:preauth, amount, {}, credit_card, options) + end + + def purchase(amount, credit_card, options = {}) + complete_order_request(:purchase, amount, credit_card, options) + end + + def authorize(amount, credit_card, options = {}) + complete_order_request(:authorize, amount, credit_card, options) + end + + def capture(amount, authorization, options = {}) + operation_request(:capture, amount, authorization, options) + end + + def refund(amount, authorization, options = {}) + operation_request(:refund, amount, authorization, options) + end + + def verify(credit_card, options = {}) + post = {} + add_invoice(post, 0, options) + add_customer_data(post, credit_card, options) + add_credit_card(post, credit_card) + commit(:verify, post, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((X-Api-Key: )(\w|-)+), '\1[FILTERED]'). + gsub(%r(("pan\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cvv\\?":\\?")\d+), '\1[FILTERED]') + end + + private + + def validation(options = {}) + post = {} + add_3ds_validation_params(post, options) + commit(:validation, post, options) + end + + def complete_order_request(action, amount, credit_card, options = {}) + MultiResponse.run do |r| + r.process { validation(options) } + r.process { order_request(action, amount, { captureType: (action == :authorize ? 'EXPLICIT' : 'IMPLICIT') }, credit_card, options.merge!(validation: r.params)) } + end + end + + def order_request(action, amount, post, credit_card, options = {}) + add_invoice(post, amount, options) + add_credit_card(post, credit_card) + add_customer_data(post, credit_card, options) + add_address(post, options) + add_recurrence(post, options) unless options[:operation_id] + add_exemptions(post, options) + add_3ds_params(post, options[:validation]) if options[:validation] + + commit(action, post, options) + end + + def operation_request(action, amount, authorization, options) + options[:correlation_id], options[:reference] = authorization.split('#') + commit(action, { amount: amount, currency: options[:currency] }, options) + end + + def add_invoice(post, amount, options) + currency = options[:currency] || currency(amount) + post[:order] = { + orderId: options[:order_id], + amount: localized_amount(amount, currency), + currency: currency + }.compact + end + + def add_credit_card(post, credit_card) + post[:card] = { + pan: credit_card.number, + expiryDate: expdate(credit_card), + cvv: credit_card.verification_value + } + end + + def add_customer_data(post, credit_card, options) + post[:order][:customerInfo] = { + cardHolderName: credit_card.name, + cardHolderEmail: options[:email] + }.compact + end + + def add_address(post, options) + if address = options[:billing_address] || options[:address] + post[:order][:customerInfo][:billingAddress] = { + name: address[:name], + street: address[:address1], + additionalInfo: address[:address2], + city: address[:city], + postCode: address[:zip], + country: address[:country] + }.compact + end + + if address = options[:shipping_address] + post[:order][:customerInfo][:shippingAddress] = { + name: address[:name], + street: address[:address1], + additionalInfo: address[:address2], + city: address[:city], + postCode: address[:zip], + country: address[:country] + }.compact + end + end + + def add_recurrence(post, options) + post[:recurrence] = { action: options[:recurrence] || 'NO_RECURRING' } + end + + def add_exemptions(post, options) + post[:exemptions] = options[:exemptions] || 'NO_PREFERENCE' + end + + def add_3ds_params(post, validation) + post[:threeDSAuthData] = { + authenticationValue: validation['threeDSAuthResult']['authenticationValue'], + eci: validation['threeDSAuthResult']['eci'], + xid: validation['threeDSAuthResult']['xid'] + } + post[:operationId] = validation['operation']['operationId'] + end + + def add_3ds_validation_params(post, options) + post[:operationId] = options[:operation_id] + post[:threeDSAuthResponse] = options[:three_ds_auth_response] + end + + def parse(body) + JSON.parse(body) + end + + def commit(action, params, options) + options[:correlation_id] ||= SecureRandom.uuid + transaction_id = transaction_id_from(params, options, action) + raw_response = + begin + url = build_request_url(action, transaction_id) + ssl_post(url, params.to_json, request_headers(options, action)) + rescue ResponseError => e + { errors: [code: e.response.code, description: e.response.body] }.to_json + end + response = parse(raw_response) + + Response.new( + success_from(action, response), + message_from(response), + response, + authorization: authorization_from(options[:correlation_id], response), + test: test?, + error_code: error_code_from(response) + ) + end + + def request_headers(options, action = nil) + headers = { 'X-Api-Key' => @api_key, 'Content-Type' => 'application/json', 'Correlation-Id' => options[:correlation_id] } + headers.merge!('Idempotency-Key' => options[:idempotency_key] || SecureRandom.uuid) if %i[capture refund].include?(action) + headers + end + + def transaction_id_from(params, options, action = nil) + case action + when :refund, :capture + return options[:reference] + else + return params[:operation_id] + end + end + + def build_request_url(action, id = nil) + "#{test? ? test_url : live_url}#{ENDPOINTS_MAPPING[action.to_sym] % id}" + end + + def success_from(action, response) + case action + when :capture, :refund + response.include?('operationId') && response.include?('operationTime') + else + SUCCESS_MESSAGES.include?(response.dig('operation', 'operationResult')) + end + end + + def message_from(response) + response['operationId'] || response.dig('operation', 'operationResult') || response.dig('errors', 0, 'description') + end + + def authorization_from(correlation_id, response = {}) + [correlation_id, (response['operationId'] || response.dig('operation', 'operationId'))].join('#') + end + + def error_code_from(response) + response.dig('errors', 0, 'code') + end + end + end +end diff --git a/lib/active_merchant/billing/network_tokenization_credit_card.rb b/lib/active_merchant/billing/network_tokenization_credit_card.rb index e4108977f80..51798547d1e 100644 --- a/lib/active_merchant/billing/network_tokenization_credit_card.rb +++ b/lib/active_merchant/billing/network_tokenization_credit_card.rb @@ -14,7 +14,7 @@ class NetworkTokenizationCreditCard < CreditCard self.require_verification_value = false self.require_name = false - attr_accessor :payment_cryptogram, :eci, :transaction_id, :metadata + attr_accessor :payment_cryptogram, :eci, :transaction_id, :metadata, :payment_data attr_writer :source SOURCES = %i(apple_pay android_pay google_pay network_token) diff --git a/lib/active_merchant/billing/response.rb b/lib/active_merchant/billing/response.rb index fb1c502d7ee..754f8f5d4a0 100644 --- a/lib/active_merchant/billing/response.rb +++ b/lib/active_merchant/billing/response.rb @@ -100,11 +100,11 @@ def cvv_result end %w(params message test authorization error_code emv_authorization test? fraud_review?).each do |m| - class_eval %( + class_eval <<~RUBY, __FILE__, __LINE__ + 1 def #{m} (@responses.empty? ? nil : primary_response.#{m}) end - ) + RUBY end end end diff --git a/lib/active_merchant/connection.rb b/lib/active_merchant/connection.rb index c15ac5bb803..c2669cd4a2e 100644 --- a/lib/active_merchant/connection.rb +++ b/lib/active_merchant/connection.rb @@ -18,27 +18,13 @@ class Connection RETRY_SAFE = false RUBY_184_POST_HEADERS = { 'Content-Type' => 'application/x-www-form-urlencoded' } - attr_accessor :endpoint - attr_accessor :open_timeout - attr_accessor :read_timeout - attr_accessor :verify_peer - attr_accessor :ssl_version + attr_accessor :endpoint, :open_timeout, :read_timeout, :verify_peer, :ssl_version, :ca_file, :ca_path, :pem, :pem_password, :logger, :tag, :ignore_http_status, :max_retries, :proxy_address, :proxy_port + if Net::HTTP.instance_methods.include?(:min_version=) attr_accessor :min_version attr_accessor :max_version end - attr_reader :ssl_connection - attr_accessor :ca_file - attr_accessor :ca_path - attr_accessor :pem - attr_accessor :pem_password - attr_reader :wiredump_device - attr_accessor :logger - attr_accessor :tag - attr_accessor :ignore_http_status - attr_accessor :max_retries - attr_accessor :proxy_address - attr_accessor :proxy_port + attr_reader :ssl_connection, :wiredump_device def initialize(endpoint) @endpoint = endpoint.is_a?(URI) ? endpoint : URI.parse(endpoint) @@ -85,8 +71,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 6fee9d6a874..11e53082993 100644 --- a/lib/active_merchant/country.rb +++ b/lib/active_merchant/country.rb @@ -9,6 +9,7 @@ class CountryCodeFormatError < StandardError class CountryCode attr_reader :value, :format + def initialize(value) @value = value.to_s.upcase detect_format diff --git a/lib/active_merchant/errors.rb b/lib/active_merchant/errors.rb index af4bcb8b1be..562629b395e 100644 --- a/lib/active_merchant/errors.rb +++ b/lib/active_merchant/errors.rb @@ -23,10 +23,23 @@ def initialize(response, message = nil) end def to_s - "Failed with #{response.code} #{response.message if response.respond_to?(:message)}" + if response.kind_of?(String) + if response.start_with?('Failed') + return response + else + return "Failed with #{response}" + end + end + + return response.message if response.respond_to?(:message) && response.message.start_with?('Failed') + + "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 a36602fe521..2a2845e4371 100644 --- a/lib/active_merchant/version.rb +++ b/lib/active_merchant/version.rb @@ -1,3 +1,3 @@ module ActiveMerchant - VERSION = '1.127.0' + VERSION = '1.136.0' end diff --git a/lib/support/gateway_support.rb b/lib/support/gateway_support.rb index c1e358db323..6d77898cafc 100644 --- a/lib/support/gateway_support.rb +++ b/lib/support/gateway_support.rb @@ -23,8 +23,8 @@ def initialize @gateways.delete(ActiveMerchant::Billing::BogusGateway) end - def each_gateway - @gateways.each { |g| yield g } + def each_gateway(&block) + @gateways.each(&block) end def features 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 66606cf5e6d..c4bb3de97ab 100644 --- a/test/fixtures.yml +++ b/test/fixtures.yml @@ -178,7 +178,8 @@ cecabank: merchant_id: MERCHANTID acquirer_bin: ACQUIRERBIN terminal_id: TERMINALID - key: KEY + signature_key: KEY + encryption_key: KEY cenpos: merchant_id: SOMECREDENTIAL @@ -198,6 +199,10 @@ checkout_v2: 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 password: 7c70414732de7e0ba3a04db5f24fcec8 @@ -272,6 +277,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 @@ -282,6 +293,10 @@ data_cash: login: X password: Y +datatrans: + merchant_id: MERCHANT_ID_WEB + password: MERCHANT_PASSWORD_WEB + # Working credentials, no need to replace decidir_authorize: api_key: 5a15fbc227224edabdb6f2e8219e8b28 @@ -298,6 +313,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 @@ -395,6 +415,11 @@ first_pay: transaction_center_id: 1264 gateway_id: "a91c38c3-7d7f-4d29-acc7-927b4dca0dbe" +first_pay_rest_json: + mode: "rest_json" + merchant_key: "a91c38c3-7d7f-4d29-acc7-927b4dca0dbe" + processor_id: "15417" + firstdata_e4: login: SD8821-67 password: T6bxSywbcccbJ19eDXNIGaCDOBg1W7T8 @@ -405,6 +430,12 @@ firstdata_e4_v27: key_id: ANINTEGER hmac_key: AMAGICALKEY +flex_charge: + app_key: 'your app key' + app_secret: 'app secret' + site_id: 'site id' + mid: 'merchant id' + flo2cash: username: SOMECREDENTIAL password: ANOTHERCREDENTIAL @@ -429,8 +460,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" @@ -441,6 +477,10 @@ hdfc: login: LOGIN password: PASSWORD +hi_pay: + username: "USERNAME" + password: "PASSWORD" + # Working credentials, no need to replace hps: secret_api_key: "skapi_cert_MYl2AQAowiQAbLp5JesGKh7QFkcizOP2jcX9BrEMqQ" @@ -461,7 +501,6 @@ instapay: login: TEST0 password: -# Working credentials, no need to replace ipg: store_id: "YOUR STORE ID" user_id: "YOUR USER ID" @@ -469,6 +508,13 @@ ipg: pem_password: "CERTIFICATE PASSWORD" pem: "YOUR CERTIFICATE WITH PRIVATE KEY" +ipg_ma: + store_id: "ONE OF YOUR STORE IDs" + user_id: "YOUR USER ID" + password: "YOUR PASSWORD" + pem_password: "CERTIFICATE PASSWORD" + pem: "YOUR CERTIFICATE WITH PRIVATE KEY" + # Working credentials, no need to replace ipp: username: nmi.api @@ -706,6 +752,11 @@ orbital_tandem_gateway: password: PASSWORD merchant_id: MERCHANTID +orbital_tpv_gateway: + login: LOGIN + password: PASSWORD + merchant_id: MERCHANTID + # Working credentials, no need to replace pagarme: api_key: 'ak_test_e1QGU2gL98MDCHZxHLJ9sofPUFJ7tH' @@ -1216,6 +1267,12 @@ redsys: login: MERCHANT CODE secret_key: SECRET KEY +# https://pagosonline.redsys.es/entornosPruebas.html +redsys_rest: + login: 999008881 + secret_key: sq7HjrUOBfKmC576ILgskD5srU870gJ7 + terminal: 001 + redsys_sha256: login: MERCHANT CODE secret_key: SECRET KEY @@ -1271,6 +1328,9 @@ 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' @@ -1304,6 +1364,14 @@ stripe_verified_bank_account: customer_id: "cus_7s22nNueP2Hjj6" bank_account_id: "ba_17cHxeAWOtgoysogv3NM8CJ1" +sum_up: + access_token: SOMECREDENTIAL + pay_to_email: SOMECREDENTIAL + +sum_up_3ds: + access_token: SOMECREDENTIAL + pay_to_email: SOMECREDENTIAL + # Working credentials, no need to replace swipe_checkout: login: 2077103073D8B5 @@ -1451,3 +1519,6 @@ worldpay_us: acctid: MPNAB subid: SPREE merchantpin: "1234567890" + +xpay: + api_key: 5d952446-9004-4023-9eae-a527a152846b diff --git a/test/remote/gateways/remote_adyen_test.rb b/test/remote/gateways/remote_adyen_test.rb index b888898b8ed..70fea7c9c1a 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? @@ -456,6 +456,7 @@ def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response assert_equal 'Refused', response.message + assert_equal 'Refused', response.error_code end def test_failed_authorize_with_bank_account @@ -530,6 +531,36 @@ def test_successful_purchase_with_idempotency_key assert_equal response.authorization, first_auth end + def test_successful_purchase_with_billing_default_country_code + options = @options.dup.update({ + billing_address: { + address1: 'Infinite Loop', + address2: 1, + country: '', + city: 'Cupertino', + state: 'CA', + zip: '95014' + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_shipping_default_country_code + options = @options.dup.update({ + shipping_address: { + address1: 'Infinite Loop', + address2: 1, + country: '', + city: 'Cupertino', + state: 'CA', + zip: '95014' + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + def test_successful_purchase_with_apple_pay response = @gateway.purchase(@amount, @apple_pay_card, @options) assert_success response @@ -775,6 +806,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 @@ -1015,11 +1076,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 @@ -1161,7 +1220,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 @@ -1206,8 +1265,7 @@ def test_missing_state_for_purchase def test_blank_country_for_purchase @options[:billing_address][:country] = '' response = @gateway.authorize(@amount, @credit_card, @options) - assert_failure response - assert_match Gateway::STANDARD_ERROR_CODE[:incorrect_address], response.error_code + assert_success response end def test_nil_state_for_purchase @@ -1216,6 +1274,12 @@ def test_nil_state_for_purchase assert_success response end + def test_nil_country_for_purchase + @options[:billing_address][:country] = nil + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + end + def test_blank_state_for_purchase @options[:billing_address][:state] = '' response = @gateway.authorize(@amount, @credit_card, @options) @@ -1343,6 +1407,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', @@ -1373,12 +1478,63 @@ 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_sending_mcc_on_authorize + options = { + reference: '345123', + email: 'john.smith@test.com', + ip: '77.110.174.153', + shopper_reference: 'John Smith', + order_id: '123', + mcc: '5411' + } + response = @gateway.authorize(@amount, @credit_card, options) + assert_failure response + assert_equal 'Could not find an acquirer account for the provided currency (USD).', response.message + end + def test_successful_authorize_with_level_2_data level_2_data = { total_tax_amount: '160', @@ -1458,6 +1614,63 @@ def test_successful_purchase_with_level_3_data 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 @@ -1512,6 +1725,33 @@ def test_successful_authorize_with_alternate_kosovo_code 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 + + def test_successful_purchase_with_metadata + metadata = { + field_one: 'A', + field_two: 'B', + field_three: 'C', + field_four: 'EASY AS ONE TWO THREE' + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(metadata: metadata)) + 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 5cbc4053c7d..24aa9fe3b61 100644 --- a/test/remote/gateways/remote_airwallex_test.rb +++ b/test/remote/gateways/remote_airwallex_test.rb @@ -14,6 +14,13 @@ def setup @stored_credential_mit_options = { initial_transaction: false, initiator: 'merchant', reason_type: 'recurring' } end + def test_failed_access_token + assert_raises(ActiveMerchant::OAuthResponseError) do + gateway = AirwallexGateway.new({ client_id: 'YOUR_CLIENT_ID', client_api_key: 'YOUR_API_KEY' }) + gateway.send :setup_access_token + end + end + def test_successful_purchase response = @gateway.purchase(@amount, @credit_card, @options) assert_success response diff --git a/test/remote/gateways/remote_alelo_test.rb b/test/remote/gateways/remote_alelo_test.rb index be4d9ae9059..8a4fef24e7b 100644 --- a/test/remote/gateways/remote_alelo_test.rb +++ b/test/remote/gateways/remote_alelo_test.rb @@ -26,7 +26,7 @@ def test_access_token_success end def test_failure_access_token_with_invalid_keys - error = assert_raises(ActiveMerchant::ResponseError) do + error = assert_raises(ActiveMerchant::OAuthResponseError) do gateway = AleloGateway.new({ client_id: 'abc123', client_secret: 'abc456' }) gateway.send :fetch_access_token end @@ -145,9 +145,11 @@ def test_successful_purchase_with_geolocalitation 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 + error = assert_raises(ActiveMerchant::OAuthResponseError) do + gateway.purchase(@amount, @credit_card, @options) + end + + assert_match(/401/, error.message) end def test_transcript_scrubbing 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 2dd8f4ef594..ea504fb3171 100644 --- a/test/remote/gateways/remote_authorize_net_test.rb +++ b/test/remote/gateways/remote_authorize_net_test.rb @@ -175,6 +175,16 @@ def test_successful_purchase_with_level_2_and_3_data assert_equal 'This transaction has been approved', response.message end + def test_successful_purchase_with_surcharge + options = @options.merge(surcharge: { + amount: 20, + description: 'test description' + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'This transaction has been approved', response.message + end + def test_successful_purchase_with_customer response = @gateway.purchase(@amount, @credit_card, @options.merge(customer: 'abcd_123')) assert_success response @@ -414,6 +424,16 @@ 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 @@ -448,8 +468,7 @@ def test_successful_verify_after_store_with_custom_verify_amount end def test_successful_verify_with_apple_pay - credit_card = network_tokenization_credit_card('4242424242424242', - payment_cryptogram: '111111111100cryptogram') + 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 @@ -825,6 +844,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 @@ -850,9 +881,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 @@ -862,9 +895,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 @@ -877,9 +912,11 @@ def test_successful_refund_with_network_tokenization end def test_successful_credit_with_network_tokenization - credit_card = network_tokenization_credit_card('4000100011112224', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - verification_value: nil) + credit_card = network_tokenization_credit_card( + '5424000000000015', + payment_cryptogram: 'EjRWeJASNFZ4kBI0VniQEjRWeJA=', + verification_value: nil + ) response = @gateway.credit(@amount, credit_card, @options) assert_success response @@ -888,10 +925,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_blue_snap_test.rb b/test/remote/gateways/remote_blue_snap_test.rb index 3eb143e6986..c5099c4aa04 100644 --- a/test/remote/gateways/remote_blue_snap_test.rb +++ b/test/remote/gateways/remote_blue_snap_test.rb @@ -6,13 +6,13 @@ def setup @amount = 100 @credit_card = credit_card('4263982640269299') - @cabal_card = credit_card('6271701225979642', month: 3, year: 2024) - @naranja_card = credit_card('5895626746595650', month: 11, year: 2024) - @declined_card = credit_card('4917484589897107', month: 1, year: 2023) - @invalid_card = credit_card('4917484589897106', month: 1, year: 2023) - @three_ds_visa_card = credit_card('4000000000001091', month: 1) - @three_ds_master_card = credit_card('5200000000001096', month: 1) - @invalid_cabal_card = credit_card('5896 5700 0000 0000', month: 1, year: 2023) + @cabal_card = credit_card('6271701225979642') + @naranja_card = credit_card('5895626746595650') + @declined_card = credit_card('4917484589897107') + @invalid_card = credit_card('4917484589897106') + @three_ds_visa_card = credit_card('4000000000001091') + @three_ds_master_card = credit_card('5200000000001096') + @invalid_cabal_card = credit_card('5896 5700 0000 0000') # BlueSnap may require support contact to activate fraud checking on sandbox accounts. # Specific merchant-configurable thresholds can be set as follows: @@ -292,7 +292,7 @@ def test_successful_purchase_with_currency end def test_successful_purchase_with_level3_data - l_three_visa = credit_card('4111111111111111', month: 2, year: 2023) + l_three_visa = credit_card('4111111111111111') options = @options.merge({ customer_reference_number: '1234A', sales_tax_amount: 0.6, @@ -447,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 diff --git a/test/remote/gateways/remote_borgun_test.rb b/test/remote/gateways/remote_borgun_test.rb index 4b6771917dd..1108632f95f 100644 --- a/test/remote/gateways/remote_borgun_test.rb +++ b/test/remote/gateways/remote_borgun_test.rb @@ -30,14 +30,14 @@ def test_successful_purchase end def test_successful_preauth_3ds - response = @gateway.purchase(@amount, @credit_card, @options.merge({ merchant_return_url: 'http://localhost/index.html', apply_3d_secure: '1' })) + 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({ merchant_return_url: 'http://localhost/index.html', apply_3d_secure: '1' })) + 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'] @@ -187,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 842218143e1..859dacf1288 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 = { @@ -90,6 +103,14 @@ def test_successful_setup_purchase assert_not_nil response.params['client_token'] end + def test_successful_setup_purchase_with_merchant_account_id + assert response = @gateway.setup_purchase(merchant_account_id: fixtures(:braintree_blue)[:merchant_account_id]) + 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 @@ -201,6 +222,15 @@ 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. @@ -236,8 +266,9 @@ def test_failed_verify def test_successful_credit_card_verification card = credit_card('4111111111111111') - assert response = @gateway.verify(card, @options.merge({ allow_card_verification: true })) + assert response = @gateway.verify(card, @options.merge({ allow_card_verification: true, merchant_account_id: fixtures(:braintree_blue)[:merchant_account_id] })) assert_success response + assert_match 'OK', response.message assert_equal 'M', response.cvv_result['code'] assert_equal 'P', response.avs_result['code'] @@ -461,8 +492,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'] @@ -482,6 +512,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 @@ -512,7 +551,7 @@ def test_successful_purchase_with_device_data assert transaction = response.params['braintree_transaction'] assert transaction['risk_data'] assert transaction['risk_data']['id'] - assert_equal 'Approve', transaction['risk_data']['decision'] + assert_equal true, ['Not Evaluated', 'Approve'].include?(transaction['risk_data']['decision']) assert_equal false, transaction['risk_data']['device_data_captured'] assert_equal 'fraud_protection', transaction['risk_data']['fraud_service_provider'] end @@ -552,9 +591,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' ) @@ -586,9 +623,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'] @@ -607,17 +647,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 @@ -651,27 +692,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 @@ -682,13 +708,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 @@ -799,9 +827,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' ) @@ -820,9 +846,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' ) @@ -915,6 +939,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.' @@ -928,25 +971,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 @@ -974,6 +1023,43 @@ def test_verify_credentials assert !gateway.verify_credentials end + def test_successful_recurring_first_stored_credential_v2 + creds_options = stored_credential_options(:cardholder, :recurring, :initial) + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options, stored_credentials_v2: true)) + assert_success response + assert_equal '1000 Approved', response.message + assert_not_nil response.params['braintree_transaction']['network_transaction_id'] + assert_equal 'submitted_for_settlement', response.params['braintree_transaction']['status'] + end + + def test_successful_follow_on_recurring_first_cit_stored_credential_v2 + creds_options = stored_credential_options(:cardholder, :recurring, id: '020190722142652') + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options, stored_credentials_v2: true)) + assert_success response + assert_equal '1000 Approved', response.message + assert_not_nil response.params['braintree_transaction']['network_transaction_id'] + assert_equal 'submitted_for_settlement', response.params['braintree_transaction']['status'] + end + + def test_successful_follow_on_recurring_first_mit_stored_credential_v2 + creds_options = stored_credential_options(:merchant, :recurring, id: '020190722142652') + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options, stored_credentials_v2: true)) + assert_success response + assert_equal '1000 Approved', response.message + assert_not_nil response.params['braintree_transaction']['network_transaction_id'] + assert_equal 'submitted_for_settlement', response.params['braintree_transaction']['status'] + end + + def test_successful_one_time_mit_stored_credential_v2 + creds_options = stored_credential_options(:merchant, id: '020190722142652') + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options, stored_credentials_v2: true)) + + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'submitted_for_settlement', response.params['braintree_transaction']['status'] + assert_not_nil response.params['braintree_transaction']['network_transaction_id'] + end + def test_successful_merchant_purchase_initial creds_options = stored_credential_options(:merchant, :recurring, :initial) response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options)) @@ -1182,6 +1268,144 @@ 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_and_return_paypal_details_object + @non_payal_link_gateway = BraintreeGateway.new(fixtures(:braintree_blue_non_linked_paypal)) + assert response = @non_payal_link_gateway.purchase(400000, 'fake-paypal-one-time-nonce', @options.merge(payment_method_nonce: 'fake-paypal-one-time-nonce')) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'paypal_payer_id', response.params['braintree_transaction']['paypal_details']['payer_id'] + assert_equal 'payer@example.com', response.params['braintree_transaction']['paypal_details']['payer_email'] + end + + def test_successful_credit_card_purchase_with_prepaid_debit_issuing_bank + assert response = @gateway.purchase(@amount, @credit_card) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'credit_card', response.params['braintree_transaction']['payment_instrument_type'] + 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_unsuccessful_credit_card_purchase_and_return_payment_details + 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']) + assert_equal 'credit_card', response.params['braintree_transaction']['payment_instrument_type'] + assert_equal 'Unknown', response.params['braintree_transaction']['credit_card_details']['prepaid'] + assert_equal 'Unknown', response.params['braintree_transaction']['credit_card_details']['debit'] + assert_equal 'M', response.params.dig('braintree_transaction', 'cvv_response_code') + assert_equal 'I', response.params.dig('braintree_transaction', 'avs_response_code') + assert_equal 'Call Issuer. Pick Up Card.', response.params.dig('braintree_transaction', 'gateway_message') + assert_equal 'Unknown', response.params.dig('braintree_transaction', 'credit_card_details', 'country_of_issuance') + assert_equal 'Unknown', response.params['braintree_transaction']['credit_card_details']['issuing_bank'] + end + + def test_successful_network_token_purchase_with_prepaid_debit_issuing_bank + assert response = @gateway.purchase(@amount, @nt_credit_card) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'network_token', response.params['braintree_transaction']['payment_instrument_type'] + assert_equal 'Unknown', response.params['braintree_transaction']['network_token_details']['prepaid'] + assert_equal 'Unknown', response.params['braintree_transaction']['network_token_details']['debit'] + assert_equal 'Unknown', response.params['braintree_transaction']['network_token_details']['issuing_bank'] + end + + def test_unsuccessful_network_token_purchase_and_return_payment_details + assert response = @gateway.purchase(204700, @nt_credit_card) + assert_failure response + assert_equal('2047 : Call Issuer. Pick Up Card.', response.params['braintree_transaction']['additional_processor_response']) + assert_equal 'network_token', response.params['braintree_transaction']['payment_instrument_type'] + assert_equal 'Unknown', response.params['braintree_transaction']['network_token_details']['prepaid'] + assert_equal 'Unknown', response.params['braintree_transaction']['network_token_details']['debit'] + assert_equal 'Unknown', response.params['braintree_transaction']['network_token_details']['issuing_bank'] + end + + def test_successful_google_pay_purchase_with_prepaid_debit + credit_card = network_tokenization_credit_card( + '4111111111111111', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: '2024', + source: :google_pay, + transaction_id: '123456789', + eci: '05' + ) + + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'android_pay_card', response.params['braintree_transaction']['payment_instrument_type'] + assert_equal 'Unknown', response.params['braintree_transaction']['google_pay_details']['prepaid'] + assert_equal 'Unknown', response.params['braintree_transaction']['google_pay_details']['debit'] + end + + def test_unsuccessful_google_pay_purchase_and_return_payment_details + credit_card = network_tokenization_credit_card( + '4111111111111111', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: '2024', + source: :google_pay, + transaction_id: '123456789', + eci: '05' + ) + assert response = @gateway.purchase(204700, credit_card, @options) + assert_failure response + assert_equal('2047 : Call Issuer. Pick Up Card.', response.params['braintree_transaction']['additional_processor_response']) + assert_equal 'android_pay_card', response.params['braintree_transaction']['payment_instrument_type'] + assert_equal 'Unknown', response.params['braintree_transaction']['google_pay_details']['prepaid'] + assert_equal 'Unknown', response.params['braintree_transaction']['google_pay_details']['debit'] + end + + def test_successful_apple_pay_purchase_with_prepaid_debit_issuing_bank + credit_card = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + + assert response = @gateway.purchase(@amount, credit_card, @options) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'apple_pay_card', response.params['braintree_transaction']['payment_instrument_type'] + assert_equal 'Unknown', response.params['braintree_transaction']['apple_pay_details']['prepaid'] + assert_equal 'Unknown', response.params['braintree_transaction']['apple_pay_details']['debit'] + assert_equal 'Unknown', response.params['braintree_transaction']['apple_pay_details']['issuing_bank'] + end + + def test_unsuccessful_apple_pay_purchase_and_return_payment_details + credit_card = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + + assert response = @gateway.purchase(204700, credit_card, @options) + assert_failure response + assert_equal('2047 : Call Issuer. Pick Up Card.', response.params['braintree_transaction']['additional_processor_response']) + assert_equal 'apple_pay_card', response.params['braintree_transaction']['payment_instrument_type'] + assert_equal 'Unknown', response.params['braintree_transaction']['apple_pay_details']['prepaid'] + assert_equal 'Unknown', response.params['braintree_transaction']['apple_pay_details']['debit'] + assert_equal 'Unknown', response.params['braintree_transaction']['apple_pay_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_braintree_token_nonce_test.rb b/test/remote/gateways/remote_braintree_token_nonce_test.rb index 312af9361b6..cbc8dbc3c24 100644 --- a/test/remote/gateways/remote_braintree_token_nonce_test.rb +++ b/test/remote/gateways/remote_braintree_token_nonce_test.rb @@ -47,7 +47,7 @@ def test_unsucesfull_create_token_with_invalid_state tokenized_bank_account, err_messages = generator.create_token_nonce_for_payment_method(bank_account) assert_nil tokenized_bank_account - assert_equal "Field 'state' of variable 'input' has coerced Null value for NonNull type 'UsStateCode!'", err_messages + assert_equal "Variable 'input' has an invalid value: Field 'state' has coerced Null value for NonNull type 'UsStateCode!'", err_messages end def test_unsucesfull_create_token_with_invalid_zip_code @@ -57,7 +57,7 @@ def test_unsucesfull_create_token_with_invalid_zip_code tokenized_bank_account, err_messages = generator.create_token_nonce_for_payment_method(bank_account) assert_nil tokenized_bank_account - assert_equal "Field 'zipCode' of variable 'input' has coerced Null value for NonNull type 'UsZipCode!'", err_messages + assert_equal "Variable 'input' has an invalid value: Field 'zipCode' has coerced Null value for NonNull type 'UsZipCode!'", err_messages end def test_url_generation @@ -80,4 +80,13 @@ def test_url_generation assert_equal 'https://payments.braintree-api.com/graphql', generator.url end + + def test_successfully_create_token_nonce_for_credit_card + generator = TokenNonce.new(@braintree_backend, @options) + credit_card = credit_card('4111111111111111') + tokenized_credit_card, err_messages = generator.create_token_nonce_for_payment_method(credit_card) + assert_not_nil tokenized_credit_card + assert_match %r(^tokencc_), tokenized_credit_card + assert_nil err_messages + end end diff --git a/test/remote/gateways/remote_card_connect_test.rb b/test/remote/gateways/remote_card_connect_test.rb index 68301145fd6..4717bb2f2b8 100644 --- a/test/remote/gateways/remote_card_connect_test.rb +++ b/test/remote/gateways/remote_card_connect_test.rb @@ -123,9 +123,9 @@ def test_successful_purchase_with_user_fields order_date: '20170507', ship_from_date: '20877', user_fields: [ - { 'udf0': 'value0' }, - { 'udf1': 'value1' }, - { 'udf2': 'value2' } + { udf0: 'value0' }, + { udf1: 'value1' }, + { udf2: 'value2' } ] } diff --git a/test/remote/gateways/remote_card_stream_test.rb b/test/remote/gateways/remote_card_stream_test.rb index 046ee4e4d3a..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,12 @@ 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, diff --git a/test/remote/gateways/remote_cecabank_rest_json_test.rb b/test/remote/gateways/remote_cecabank_rest_json_test.rb new file mode 100644 index 00000000000..f0c23f98f7c --- /dev/null +++ b/test/remote/gateways/remote_cecabank_rest_json_test.rb @@ -0,0 +1,181 @@ +require 'test_helper' + +class RemoteCecabankTest < Test::Unit::TestCase + def setup + @gateway = CecabankJsonGateway.new(fixtures(:cecabank)) + + @amount = 100 + @credit_card = credit_card('4507670001000009', { month: 12, year: Time.now.year, verification_value: '989' }) + @declined_card = credit_card('5540500001000004', { month: 11, year: Time.now.year + 1, verification_value: '001' }) + + @options = { + order_id: generate_unique_id, + three_d_secure: three_d_secure + } + + @cit_options = @options.merge({ + recurring_end_date: "#{Time.now.year}1231", + recurring_frequency: '1', + stored_credential: { + reason_type: 'unscheduled', + initiator: 'cardholder' + } + }) + end + + def test_successful_authorize + assert response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal %i[codAut numAut referencia], JSON.parse(response.message).symbolize_keys.keys.sort + end + + def test_unsuccessful_authorize + assert response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_match @gateway.options[:merchant_id], response.message + assert_match '190', response.error_code + end + + def test_successful_capture + assert authorize = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize + assert response = @gateway.capture(@amount, authorize.authorization, @options) + assert_success response + assert_equal %i[codAut numAut referencia], JSON.parse(response.message).symbolize_keys.keys.sort + end + + def test_unsuccessful_capture + assert response = @gateway.capture(@amount, 'abc123', @options) + assert_failure response + assert_match @gateway.options[:merchant_id], response.message + assert_match '807', response.error_code + end + + def test_successful_purchase + assert response = @gateway.purchase(@amount, @credit_card, order_id: generate_unique_id) + assert_success response + assert_equal %i[codAut numAut referencia], JSON.parse(response.message).symbolize_keys.keys.sort + end + + def test_unsuccessful_purchase + assert response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_match @gateway.options[:merchant_id], response.message + assert_match '190', response.error_code + end + + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert response = @gateway.refund(@amount, purchase.authorization, order_id: @options[:order_id]) + assert_success response + assert_equal %i[acquirerBIN codAut importe merchantID numAut numOperacion pais referencia terminalID tipoOperacion], JSON.parse(response.message).symbolize_keys.keys.sort + end + + def test_unsuccessful_refund + assert response = @gateway.refund(@amount, 'reference', @options) + assert_failure response + assert_match @gateway.options[:merchant_id], response.message + assert_match '15', response.error_code + end + + def test_successful_void + authorize = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize + + assert response = @gateway.void(authorize.authorization, order_id: @options[:order_id]) + assert_success response + assert_equal %i[acquirerBIN codAut importe merchantID numAut numOperacion pais referencia terminalID tipoOperacion], JSON.parse(response.message).symbolize_keys.keys.sort + end + + def test_unsuccessful_void + assert response = @gateway.void('reference', { order_id: generate_unique_id }) + assert_failure response + assert_match @gateway.options[:merchant_id], response.message + assert_match '15', response.error_code + end + + def test_invalid_login + gateway = CecabankGateway.new(fixtures(:cecabank).merge(cypher_key: 'invalid')) + + assert response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match 'ERROR AL CALCULAR FIRMA', response.message + end + + def test_purchase_using_stored_credential_cit + assert purchase = @gateway.purchase(@amount, @credit_card, @cit_options) + assert_success purchase + end + + def test_purchase_stored_credential_with_network_transaction_id + @cit_options.merge!({ network_transaction_id: '999999999999999' }) + assert purchase = @gateway.purchase(@amount, @credit_card, @cit_options) + assert_success purchase + end + + def test_purchase_using_auth_capture_and_stored_credential_cit + assert authorize = @gateway.authorize(@amount, @credit_card, @cit_options) + assert_success authorize + assert_equal authorize.network_transaction_id, '999999999999999' + + assert capture = @gateway.capture(@amount, authorize.authorization, @options) + assert_success capture + end + + def test_purchase_using_stored_credential_recurring_mit + @cit_options[:stored_credential][:reason_type] = 'installment' + assert purchase = @gateway.purchase(@amount, @credit_card, @cit_options) + assert_success purchase + + options = @cit_options.except(:three_d_secure, :extra_options_for_three_d_secure) + options[:stored_credential][:reason_type] = 'recurring' + options[:stored_credential][:initiator] = 'merchant' + options[:stored_credential][:network_transaction_id] = purchase.network_transaction_id + + assert purchase2 = @gateway.purchase(@amount, @credit_card, options) + assert_success purchase2 + end + + def test_failure_stored_credential_invalid_cit_transaction_id + options = @cit_options + options[:stored_credential][:reason_type] = 'recurring' + options[:stored_credential][:initiator] = 'merchant' + options[:stored_credential][:network_transaction_id] = 'bad_reference' + + assert purchase = @gateway.purchase(@amount, @credit_card, options) + assert_failure purchase + assert_match @gateway.options[:merchant_id], purchase.message + assert_match '810', purchase.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) + end + + private + + def get_response_params(transcript) + response = JSON.parse(transcript.gsub(%r(\\\")i, "'").scan(/{[^>]*}/).first.gsub("'", '"')) + JSON.parse(Base64.decode64(response['parametros'])) + end + + def three_d_secure + { + version: '2.2.0', + eci: '02', + cavv: '4F80DF50ADB0F9502B91618E9B704790EABA35FDFC972DDDD0BF498C6A75E492', + ds_transaction_id: 'a2bf089f-cefc-4d2c-850f-9153827fe070', + acs_transaction_id: '18c353b0-76e3-4a4c-8033-f14fe9ce39dc', + authentication_response_status: 'Y', + three_ds_server_trans_id: '9bd9aa9c-3beb-4012-8e52-214cccb25ec5' + } + end +end diff --git a/test/remote/gateways/remote_cecabank_test.rb b/test/remote/gateways/remote_cecabank_test.rb index d0389ba26fc..217ed8cc501 100644 --- a/test/remote/gateways/remote_cecabank_test.rb +++ b/test/remote/gateways/remote_cecabank_test.rb @@ -41,11 +41,11 @@ def test_unsuccessful_refund assert response = @gateway.refund(@amount, purchase.authorization, @options.merge(currency: 'USD')) assert_failure response - assert_match 'ERROR', response.message + assert_match 'Error', response.message end def test_invalid_login - gateway = CecabankGateway.new(fixtures(:cecabank).merge(key: 'invalid')) + gateway = CecabankGateway.new(fixtures(:cecabank).merge(signature_key: 'invalid')) assert response = gateway.purchase(@amount, @credit_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_checkout_v2_test.rb b/test/remote/gateways/remote_checkout_v2_test.rb index 4a212d8790c..4347b82842c 100644 --- a/test/remote/gateways/remote_checkout_v2_test.rb +++ b/test/remote/gateways/remote_checkout_v2_test.rb @@ -1,68 +1,85 @@ +require 'timecop' require 'test_helper' class RemoteCheckoutV2Test < Test::Unit::TestCase def setup 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', processing_channel_id: 'pc_lxgl7aqahkzubkundd2l546hdm' @@ -71,10 +88,7 @@ def setup 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, @@ -99,6 +113,68 @@ def setup 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 + + def test_failed_access_token + assert_raises(ActiveMerchant::OAuthResponseError) do + gateway = CheckoutV2Gateway.new({ client_id: 'YOUR_CLIENT_ID', client_secret: 'YOUR_CLIENT_SECRET' }) + gateway.send :setup_access_token + end + end + + def test_failed_purchase_with_failed_oauth_credentials + error = assert_raises(ActiveMerchant::OAuthResponseError) do + gateway = CheckoutV2Gateway.new({ client_id: 'YOUR_CLIENT_ID', client_secret: 'YOUR_CLIENT_SECRET' }) + gateway.purchase(@amount, @credit_card, @options) + end + + assert_equal error.message, 'Failed with 400 Bad Request' end def test_transcript_scrubbing @@ -122,6 +198,7 @@ def test_transcript_scrubbing_via_oauth assert_scrubbed(declined_card.verification_value, transcript) assert_scrubbed(@gateway_oauth.options[:client_id], transcript) assert_scrubbed(@gateway_oauth.options[:client_secret], transcript) + assert_scrubbed(@gateway_oauth.options[:access_token], transcript) end def test_network_transaction_scrubbing @@ -134,6 +211,16 @@ def test_network_transaction_scrubbing 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 @@ -146,6 +233,53 @@ def test_successful_purchase_via_oauth assert_equal 'Succeeded', response.message end + def test_successful_purchase_via_oauth_with_access_token + assert_nil @gateway_oauth.options[:access_token] + assert_nil @gateway_oauth.options[:expires] + purchase = @gateway_oauth.purchase(@amount, @credit_card, @options) + assert_success purchase + access_token = @gateway_oauth.options[:access_token] + expires = @gateway_oauth.options[:expires] + response = @gateway_oauth.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal @gateway_oauth.options[:access_token], access_token + assert_equal @gateway_oauth.options[:expires], expires + end + + def test_failure_purchase_via_oauth_with_invalid_access_token_without_expires + @gateway_oauth.options[:access_token] = 'ABC123' + @gateway_oauth.options[:expires] = DateTime.now.strftime('%Q').to_i + 3600.seconds + + response = @gateway_oauth.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal '401: Unauthorized', response.message + assert_equal @gateway_oauth.options[:access_token], '' + end + + def test_successful_purchase_via_oauth_with_invalid_access_token_with_correct_expires + @gateway_oauth.options[:access_token] = 'ABC123' + response = @gateway_oauth.purchase(@amount, @credit_card, @options) + assert_success response + assert_not_equal 'ABC123', @gateway_oauth.options[:access_token] + end + + def test_successful_purchase_with_an_expired_access_token + initial_access_token = @gateway_oauth.options[:access_token] = SecureRandom.alphanumeric(10) + initial_expires = @gateway_oauth.options[:expires] = DateTime.now.strftime('%Q').to_i + + Timecop.freeze(DateTime.now + 1.hour) do + purchase = @gateway_oauth.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert_equal 2, purchase.responses.size + assert_not_equal initial_access_token, @gateway_oauth.options[:access_token] + assert_not_equal initial_expires, @gateway.options[:expires] + + assert_not_nil purchase.responses.first.params['access_token'] + assert_not_nil purchase.responses.first.params['expires'] + end + end + def test_successful_purchase_with_vts_network_token response = @gateway.purchase(100, @vts_network_token, @options) assert_success response @@ -328,6 +462,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 @@ -396,6 +536,120 @@ def test_successful_purchase_with_metadata assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_processing_data + options = @options.merge( + processing: { + aft: true, + preferred_scheme: 'cartes_bancaires', + app_id: 'com.iap.linker_portal', + airline_data: [ + { + ticket: { + number: '045-21351455613', + issue_date: '2023-05-20', + issuing_carrier_code: 'AI', + travel_package_indicator: 'B', + travel_agency_name: 'World Tours', + travel_agency_code: '01' + }, + passenger: [ + { + first_name: 'John', + last_name: 'White', + date_of_birth: '1990-05-26', + address: { + country: 'US' + } + } + ], + flight_leg_details: [ + { + flight_number: '101', + carrier_code: 'BA', + class_of_travelling: 'J', + departure_airport: 'LHR', + departure_date: '2023-06-19', + departure_time: '15:30', + arrival_airport: 'LAX', + stop_over_code: 'x', + fare_basis_code: 'SPRSVR' + } + ] + } + ], + partner_customer_id: '2102209000001106125F8', + partner_payment_id: '440644309099499894406', + tax_amount: '1000', + purchase_country: 'GB', + locale: 'en-US', + retrieval_reference_number: '909913440644', + partner_order_id: 'string', + partner_status: 'string', + partner_transaction_id: 'string', + partner_error_codes: [], + partner_error_message: 'string', + partner_authorization_code: 'string', + partner_authorization_response_code: 'string', + fraud_status: 'string' + } + ) + + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_recipient_data + options = @options.merge( + recipient: { + dob: '1985-05-15', + account_number: '5555554444', + zip: 'SW1A', + first_name: 'john', + last_name: 'johnny', + address: { + address1: '123 High St.', + address2: 'Flat 456', + city: 'London', + state: 'str', + zip: 'SW1A 1AA', + country: 'GB' + } + } + ) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_sender_data + options = @options.merge( + sender: { + type: 'individual', + dob: '1985-05-15', + first_name: 'Jane', + last_name: 'Doe', + address: { + address_line1: '123 High St.', + address_line2: 'Flat 456', + city: 'London', + state: 'str', + zip: 'SW1A 1AA', + country: 'GB' + }, + reference: '8285282045818', + identification: { + type: 'passport', + number: 'ABC123', + issuing_country: 'GB' + } + } + ) + response = @gateway_oauth.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_metadata_via_oauth options = @options.merge( metadata: { @@ -414,8 +668,22 @@ def test_successful_purchase_with_minimal_options 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 @@ -427,9 +695,9 @@ 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 @@ -450,6 +718,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 @@ -546,9 +820,9 @@ 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 @@ -583,6 +857,122 @@ def test_successful_credit 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', payout: true)) + 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_money_transfer_payout_handles_blank_destination_address + @payout_options[:billing_address] = nil + 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_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 @@ -648,6 +1038,15 @@ 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 @@ -683,21 +1082,16 @@ def test_failed_void_via_oauth def test_successful_verify response = @gateway.verify(@credit_card, @options) - # this should only be a Response and not a MultiResponse - # as we are passing in a 0 amount and there should be - # no void call - assert_instance_of(Response, response) - refute_instance_of(MultiResponse, response) assert_success response 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 + assert_not_nil response.responses.first.params['access_token'] + assert_not_nil response.responses.first.params['expires'] end def test_failed_verify @@ -712,4 +1106,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_clearhaus_test.rb b/test/remote/gateways/remote_clearhaus_test.rb index 844b748aee4..dfe1fd1b07d 100644 --- a/test/remote/gateways/remote_clearhaus_test.rb +++ b/test/remote/gateways/remote_clearhaus_test.rb @@ -44,7 +44,7 @@ def test_unsuccessful_signing_request assert gateway.options[:private_key] assert auth = gateway.authorize(@amount, @credit_card, @options) assert_failure auth - assert_equal 'Neither PUB key nor PRIV key: not enough data', auth.message + assert_equal 'Neither PUB key nor PRIV key: unsupported', auth.message credentials = fixtures(:clearhaus_secure) credentials[:signing_key] = 'foo' diff --git a/test/remote/gateways/remote_commerce_hub_test.rb b/test/remote/gateways/remote_commerce_hub_test.rb index 1f8d32a453e..d52c1d93a29 100644 --- a/test/remote/gateways/remote_commerce_hub_test.rb +++ b/test/remote/gateways/remote_commerce_hub_test.rb @@ -2,33 +2,66 @@ 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: '111') - @google_pay = network_tokenization_credit_card('4005550000000019', + @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', + 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', + 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=') + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) @declined_card = credit_card('4000300011112220', month: '02', year: '2035', verification_value: '123') + @master_card = credit_card('5454545454545454', brand: 'master') @options = {} + @three_d_secure = { + ds_transaction_id: '3543-b90d-d6dc1765c98', + authentication_response_status: 'A', + cavv: 'AAABCZIhcQAAAABZlyFxAAAAAAA', + eci: '05', + xid: '&x_MD5_Hash=abfaf1d1df004e3c27d5d2e05929b529&x_state=BC&x_reference_3=&x_auth_code=ET141870&x_fp_timestamp=1231877695', + version: '2.2.0' + } + @dynamic_descriptors = { + mcc: '1234', + merchant_name: 'Spreedly', + customer_service_number: '555444321', + service_entitlement: '123444555', + dynamic_descriptors_address: { + street: '123 Main Street', + houseNumberOrName: 'Unit B', + city: 'Atlanta', + stateOrProvince: 'GA', + postalCode: '30303', + country: 'US' + } + } end def test_successful_purchase @@ -37,6 +70,59 @@ def test_successful_purchase assert_equal 'Approved', response.message end + def test_successful_3ds_purchase + @options.merge!(three_d_secure: @three_d_secure) + 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 @@ -63,10 +149,17 @@ def test_successful_purchase_with_stored_credential_framework assert_success response end + def test_successful_purchase_with_dynamic_descriptors + response = @gateway.purchase(@amount, @credit_card, @options.merge(@dynamic_descriptors)) + assert_success response + assert_equal 'Approved', response.message + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'Unable to assign card to brand: Invalid.', response.message + assert_match 'Unable to assign card to brand: Invalid', response.message + assert_equal '104', response.error_code end def test_successful_authorize @@ -75,19 +168,26 @@ def test_successful_authorize 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 + 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 + capture = @gateway.capture(@amount, authorize.authorization) + assert_success capture + end + + def test_successful_authorize_and_capture_with_dynamic_descriptors + authorize = @gateway.authorize(@amount, @credit_card, @options.merge(@dynamic_descriptors)) + assert_success authorize + + capture = @gateway.capture(@amount, authorize.authorization, @options.merge(@dynamic_descriptors)) + assert_success capture + end def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_equal 'Unable to assign card to brand: Invalid.', response.message + assert_match 'Unable to assign card to brand: Invalid', response.message end def test_successful_authorize_and_void @@ -109,7 +209,28 @@ def test_failed_void def test_successful_verify response = @gateway.verify(@credit_card, @options) assert_success response - assert_equal 'Approved', response.message + 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 @@ -133,11 +254,24 @@ def test_successful_purchase_and_partial_refund end def test_failed_refund - response = @gateway.refund(nil, '123', @options) + 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_credit + response = @gateway.credit(@amount, @credit_card, @options) + + assert_success response + assert_equal 'Approved', response.message + end + + def test_failed_credit + response = @gateway.credit(@amount, '') + assert_failure response + assert_equal 'Invalid or Missing Field Data', response.message + end + def test_successful_store response = @gateway.store(@credit_card, @options) assert_success response @@ -170,7 +304,7 @@ def test_successful_purchase_with_apple_pay def test_failed_purchase_with_declined_apple_pay response = @gateway.purchase(@amount, @declined_apple_pay, @options) assert_failure response - assert_equal 'Unable to assign card to brand: Invalid.', response.message + assert_match 'Unable to assign card to brand: Invalid', response.message end def test_transcript_scrubbing @@ -182,7 +316,6 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.number, transcript) assert_scrubbed(@gateway.options[:api_key], transcript) assert_scrubbed(@gateway.options[:api_secret], transcript) - assert_scrubbed(@credit_card.verification_value, transcript) end def test_transcript_scrubbing_apple_pay @@ -196,4 +329,18 @@ def test_transcript_scrubbing_apple_pay 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_creditcall_test.rb b/test/remote/gateways/remote_creditcall_test.rb index d7ed5a7d2fa..67669780996 100644 --- a/test/remote/gateways/remote_creditcall_test.rb +++ b/test/remote/gateways/remote_creditcall_test.rb @@ -147,7 +147,7 @@ def test_failed_verify @declined_card.number = '' response = @gateway.verify(@declined_card, @options) assert_failure response - assert_match %r{PAN Must be >= 13 Digits}, response.message + assert_match %r{PAN Must be >= 12 Digits}, response.message end def test_invalid_login @@ -155,7 +155,7 @@ def test_invalid_login response = gateway.purchase(@amount, @credit_card, @options) assert_failure response - assert_match %r{Invalid TerminalID - Must be 8 digit number}, response.message + assert_match %r{Invalid terminal details}, response.message end def test_transcript_scrubbing diff --git a/test/remote/gateways/remote_credorax_test.rb b/test/remote/gateways/remote_credorax_test.rb index e7973e90c10..30b2dddab3f 100644 --- a/test/remote/gateways/remote_credorax_test.rb +++ b/test/remote/gateways/remote_credorax_test.rb @@ -45,7 +45,8 @@ def setup } } - @apple_pay_card = network_tokenization_credit_card('4176661000001015', + @apple_pay_card = network_tokenization_credit_card( + '4176661000001015', month: 10, year: Time.new.year + 2, first_name: 'John', @@ -54,15 +55,25 @@ def setup payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', eci: '07', transaction_id: 'abc123', - source: :apple_pay) + source: :apple_pay + ) - @google_pay_card = network_tokenization_credit_card('4176661000001015', + @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: '05') + 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 @@ -79,6 +90,13 @@ def test_successful_purchase_with_google_pay 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) @@ -533,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) @@ -557,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..f21d23df587 --- /dev/null +++ b/test/remote/gateways/remote_cyber_source_rest_test.rb @@ -0,0 +1,619 @@ +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') + + @visa_network_token = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :network_token + ) + @amex_network_token = network_tokenization_credit_card( + '378282246310005', + brand: 'american_express', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :network_token + ) + + @mastercard_network_token = network_tokenization_credit_card( + '5555555555554444', + brand: 'master', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :network_token + ) + + @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_visa_network_token + response = @gateway.authorize(@amount, @visa_network_token, @options) + + assert_success response + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + end + + def test_successful_authorize_with_mastercard_network_token + response = @gateway.authorize(@amount, @mastercard_network_token, @options) + + assert_success response + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + end + + def test_successful_authorize_with_amex_network_token + response = @gateway.authorize(@amount, @amex_network_token, @options) + + assert_success response + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + 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) + 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_with_discover + options = stored_credential_options(:cardholder, :recurring, :initial) + assert auth = @gateway.authorize(@amount, @discover_card, options) + assert_success auth + used_store_credentials = stored_credential_options(:cardholder, :recurring, ntid: auth.network_transaction_id) + assert purchase = @gateway.purchase(@amount, @discover_card, used_store_credentials) + assert_success purchase + end + + def test_purchase_using_stored_credential_recurring_non_us + options = stored_credential_options(:cardholder, :recurring, :initial) + options[:billing_address][:country] = 'CA' + options[:billing_address][:state] = 'ON' + options[:billing_address][:city] = 'Ottawa' + options[:billing_address][:zip] = 'K1C2N6' + assert auth = @gateway.authorize(@amount, @visa_card, options) + assert_success auth + used_store_credentials = stored_credential_options(:merchant, :recurring, ntid: auth.network_transaction_id) + assert purchase = @gateway.purchase(@amount, @visa_card, used_store_credentials) + assert_success purchase + end + + def test_purchase_using_stored_credential_recurring_cit + options = stored_credential_options(:cardholder, :recurring, :initial) + assert auth = @gateway.authorize(@amount, @visa_card, options) + assert_success auth + used_store_credentials = stored_credential_options(:cardholder, :recurring, ntid: auth.network_transaction_id) + 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) + assert auth = @gateway.authorize(@amount, @visa_card, options) + assert_success auth + used_store_credentials = stored_credential_options(:merchant, :recurring, ntid: auth.network_transaction_id) + assert purchase = @gateway.purchase(@amount, @visa_card, used_store_credentials) + assert_success purchase + end + + def test_purchase_using_stored_credential_installment + options = stored_credential_options(:cardholder, :installment, :initial) + assert auth = @gateway.authorize(@amount, @visa_card, options) + assert_success auth + used_store_credentials = stored_credential_options(:merchant, :installment, ntid: auth.network_transaction_id) + assert purchase = @gateway.authorize(@amount, @visa_card, options.merge(used_store_credentials)) + assert_success purchase + end + + def test_auth_and_purchase_with_network_txn_id + options = stored_credential_options(:merchant, :recurring, :initial) + assert auth = @gateway.authorize(@amount, @visa_card, options) + assert_success auth + 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_authorize_with_3ds2_visa + @options[:three_d_secure] = { + version: '2.2.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } + auth = @gateway.authorize(@amount, @visa_card, @options) + assert_success auth + end + + def test_successful_authorize_with_3ds2_mastercard + @options[:three_d_secure] = { + version: '2.2.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } + auth = @gateway.authorize(@amount, @master_card, @options) + assert_success auth + end + + def test_successful_purchase_with_level_2_data + response = @gateway.purchase(@amount, @visa_card, @options.merge({ purchase_order_number: '13829012412' })) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + end + + def test_successful_purchase_with_level_2_and_3_data + options = { + purchase_order_number: '6789', + discount_amount: '150', + ships_from_postal_code: '90210', + line_items: [ + { + productName: 'Product Name', + kind: 'debit', + quantity: 10, + unitPrice: '9.5000', + totalAmount: '95.00', + taxAmount: '5.00', + discountAmount: '0.00', + productCode: '54321', + commodityCode: '98765' + }, + { + productName: 'Other Product Name', + kind: 'debit', + quantity: 1, + unitPrice: '2.5000', + totalAmount: '90.00', + taxAmount: '2.00', + discountAmount: '1.00', + productCode: '54322', + commodityCode: '98766' + } + ] + } + assert response = @gateway.purchase(@amount, @visa_card, @options.merge(options)) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + end +end diff --git a/test/remote/gateways/remote_cyber_source_test.rb b/test/remote/gateways/remote_cyber_source_test.rb index 2ab0c970f54..1ff2469ca47 100644 --- a/test/remote/gateways/remote_cyber_source_test.rb +++ b/test/remote/gateways/remote_cyber_source_test.rb @@ -10,37 +10,71 @@ 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=', + source: :network_token + ) + @amex_network_token = network_tokenization_credit_card( + '378282246310005', + brand: 'american_express', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :network_token + ) + + @mastercard_network_token = network_tokenization_credit_card( + '5555555555554444', + brand: 'master', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :network_token + ) @amount = 100 @@ -70,6 +104,7 @@ def setup original_amount: '4', reference_data_code: 'ABC123', invoice_number: '123', + first_recurring_payment: true, mobile_remote_payment_type: 'A1', vat_tax_rate: '1' } @@ -106,18 +141,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 @@ -134,6 +164,13 @@ def test_successful_authorization_with_reconciliation_id assert !response.authorization.blank? end + def test_successful_authorization_with_aggregator_id + options = @options.merge(aggregator_id: 'ABCDE') + assert response = @gateway.authorize(@amount, @credit_card, options) + assert_successful_response(response) + assert !response.authorization.blank? + end + def test_successful_authorize_with_solution_id ActiveMerchant::Billing::CyberSourceGateway.application_id = 'A1000000' assert response = @gateway.authorize(@amount, @credit_card, @options) @@ -249,6 +286,38 @@ def test_successful_authorization_with_merchant_tax_id assert_successful_response(response) end + def test_successful_auth_with_single_element_from_other_tax + options = @options.merge(vat_tax_rate: '1') + + assert response = @gateway.authorize(@amount, @master_credit_card, options) + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_purchase_with_single_element_from_other_tax + options = @options.merge(national_tax_amount: '0.05') + + assert response = @gateway.purchase(@amount, @master_credit_card, options) + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_auth_with_gratuity_amount + options = @options.merge(gratuity_amount: '7.50') + + assert response = @gateway.authorize(@amount, @master_credit_card, options) + assert_successful_response(response) + assert !response.authorization.blank? + end + + def test_successful_purchase_with_gratuity_amount + options = @options.merge(gratuity_amount: '7.50') + + assert response = @gateway.purchase(@amount, @master_credit_card, options) + assert_successful_response(response) + assert !response.authorization.blank? + end + def test_successful_authorization_with_sales_slip_number options = @options.merge(sales_slip_number: '456') assert response = @gateway.authorize(@amount, @credit_card, options) @@ -372,6 +441,22 @@ def test_successful_purchase_with_bank_account 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) @@ -458,6 +543,12 @@ def test_successful_purchase_with_reconciliation_id assert_successful_response(response) end + def test_successful_purchase_with_reconciliation_id_2 + response = @gateway.purchase(@amount, @credit_card, @options) + assert_successful_response(response) + assert response.params['reconciliationID2'] + end + def test_successful_authorize_with_customer_id options = @options.merge(customer_id: '7500BB199B4270EFE05348D0AFCAD') assert response = @gateway.authorize(@amount, @credit_card, options) @@ -651,12 +742,7 @@ def test_successful_refund_with_bank_account_follow_on end def test_network_tokenization_authorize_and_capture - credit_card = network_tokenization_credit_card('4111111111111111', - brand: 'visa', - eci: '05', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') - - assert auth = @gateway.authorize(@amount, credit_card, @options) + assert auth = @gateway.authorize(@amount, @visa_network_token, @options) assert_successful_response(auth) assert capture = @gateway.capture(@amount, auth.authorization) @@ -664,12 +750,15 @@ def test_network_tokenization_authorize_and_capture end def test_network_tokenization_with_amex_cc_and_basic_cryptogram - credit_card = network_tokenization_credit_card('378282246310005', - brand: 'american_express', - eci: '05', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + assert auth = @gateway.authorize(@amount, @amex_network_token, @options) + assert_successful_response(auth) - assert auth = @gateway.authorize(@amount, credit_card, @options) + assert capture = @gateway.capture(@amount, auth.authorization) + assert_successful_response(capture) + end + + def test_network_tokenization_with_mastercard + assert auth = @gateway.authorize(@amount, @mastercard_network_token, @options) assert_successful_response(auth) assert capture = @gateway.capture(@amount, auth.authorization) @@ -680,10 +769,12 @@ 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', + credit_card = network_tokenization_credit_card( + '378282246310005', brand: 'american_express', eci: '05', - payment_cryptogram: long_cryptogram) + payment_cryptogram: long_cryptogram + ) assert auth = @gateway.authorize(@amount, credit_card, @options) assert_successful_response(auth) @@ -693,15 +784,69 @@ def test_network_tokenization_with_amex_cc_longer_cryptogram end def test_purchase_with_network_tokenization_with_amex_cc - credit_card = network_tokenization_credit_card('378282246310005', - brand: 'american_express', - eci: '05', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + 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_auth_and_capture_nt_mastercard_with_tax_options_and_no_xml_parsing_errors + credit_card = network_tokenization_credit_card('5555555555554444', + brand: 'master', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + + options = { ignore_avs: true, order_id: generate_unique_id, vat_tax_rate: 1.01 } + + assert auth = @gateway.authorize(@amount, credit_card, options) + assert_successful_response(auth) + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_successful_response(capture) + end + + def test_successful_purchase_nt_mastercard_with_tax_options_and_no_xml_parsing_errors + credit_card = network_tokenization_credit_card('5555555555554444', + brand: 'master', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + + options = { ignore_avs: true, order_id: generate_unique_id, vat_tax_rate: 1.01 } + + assert response = @gateway.purchase(@amount, credit_card, options) + assert_successful_response(response) + end + def test_successful_authorize_with_mdd_fields (1..20).each { |e| @options["mdd_field_#{e}".to_sym] = "value #{e}" } @@ -894,8 +1039,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 @@ -1133,6 +1281,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: { diff --git a/test/remote/gateways/remote_d_local_test.rb b/test/remote/gateways/remote_d_local_test.rb index 001817efef5..c46b6aeae6c 100644 --- a/test/remote/gateways/remote_d_local_test.rb +++ b/test/remote/gateways/remote_d_local_test.rb @@ -4,7 +4,7 @@ class RemoteDLocalTest < Test::Unit::TestCase def setup @gateway = DLocalGateway.new(fixtures(:d_local)) - @amount = 200 + @amount = 1000 @credit_card = credit_card('4111111111111111') @credit_card_naranja = credit_card('5895627823453005') @cabal_credit_card = credit_card('5896 5700 0000 0004') @@ -50,27 +50,34 @@ 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=') + 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')) + options = @options.merge!(stored_credential: stored_credential(:merchant, :recurring, id: 'abc123')) + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + response = @gateway.purchase(@amount, credit_card, options) 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=') + options = @options.merge!(stored_credential: stored_credential(:merchant, :recurring, id: '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'] @@ -78,13 +85,13 @@ def test_successful_purchase_with_network_tokens_and_store_credential_usage end def test_successful_purchase_with_installments - response = @gateway.purchase(@amount, @credit_card, @options_argentina_installments) + response = @gateway.purchase(@amount * 50, @credit_card, @options_argentina_installments) assert_success response assert_match 'The payment was paid', response.message end def test_successful_purchase_naranja - response = @gateway.purchase(@amount, @credit_card_naranja, @options) + response = @gateway.purchase(@amount * 50, @credit_card_naranja, @options_argentina) assert_success response assert_match 'The payment was paid', response.message end @@ -155,9 +162,9 @@ def test_successful_purchase_with_additional_data end def test_successful_purchase_with_force_type_debit - options = @options.merge(force_type: 'DEBIT') + options = @options_argentina.merge(force_type: 'DEBIT') - response = @gateway.purchase(@amount, @credit_card, options) + response = @gateway.purchase(@amount * 50, @credit_card, options) assert_success response assert_match 'The payment was paid', response.message end @@ -170,13 +177,13 @@ def test_successful_purchase_colombia end def test_successful_purchase_argentina - response = @gateway.purchase(@amount, @credit_card, @options_argentina) + response = @gateway.purchase(@amount * 50, @credit_card, @options_argentina) assert_success response assert_match 'The payment was paid', response.message end def test_successful_purchase_mexico - response = @gateway.purchase(@amount, @credit_card, @options_mexico) + response = @gateway.purchase(@amount, @cabal_credit_card, @options_mexico) assert_success response assert_match 'The payment was paid', response.message end @@ -200,8 +207,10 @@ def test_failed_purchase end def test_failed_purchase_with_network_tokens - credit_card = network_tokenization_credit_card('4242424242424242', - payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + 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 diff --git a/test/remote/gateways/remote_datatrans_test.rb b/test/remote/gateways/remote_datatrans_test.rb new file mode 100644 index 00000000000..43d74f755ed --- /dev/null +++ b/test/remote/gateways/remote_datatrans_test.rb @@ -0,0 +1,251 @@ +require 'test_helper' + +class RemoteDatatransTest < Test::Unit::TestCase + def setup + @gateway = DatatransGateway.new(fixtures(:datatrans)) + + @amount = 756 + @credit_card = credit_card('4242424242424242', verification_value: '123', first_name: 'John', last_name: 'Smith', month: 6, year: 2025) + @bad_amount = 100000 # anything grather than 500 EUR + @credit_card_frictionless = credit_card('4000001000000018', verification_value: '123', first_name: 'John', last_name: 'Smith', month: 6, year: 2025) + + @options = { + order_id: SecureRandom.random_number(1000000000).to_s, + description: 'An authorize', + email: 'john.smith@test.com' + } + + @three_d_secure = { + three_d_secure: { + eci: '05', + cavv: '3q2+78r+ur7erb7vyv66vv8=', + cavv_algorithm: '1', + xid: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'Y', + authentication_response_status: 'Y', + directory_response_status: 'Y', + version: '2', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC' + } + } + + @billing_address = address + + @google_pay_card = network_tokenization_credit_card( + '4900000000000094', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '06', + year: '2025', + source: :google_pay, + verification_value: 569 + ) + + @apple_pay_card = network_tokenization_credit_card( + '4900000000000094', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '06', + year: '2025', + source: :apple_pay, + verification_value: 569 + ) + + @nt_credit_card = network_tokenization_credit_card( + '4111111111111111', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '07', + source: :network_token, + verification_value: '737', + brand: 'visa' + ) + end + + def test_successful_authorize + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert_include response.params, 'transactionId' + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_include response.params, 'transactionId' + end + + def test_failed_authorize + # the bad amount currently is only setle to EUR currency + response = @gateway.purchase(@bad_amount, @credit_card, @options.merge({ currency: 'EUR' })) + assert_failure response + assert_equal response.error_code, 'BLOCKED_CARD' + assert_equal response.message, 'card blocked' + end + + def test_failed_authorize_invalid_currency + response = @gateway.purchase(@amount, @credit_card, @options.merge({ currency: 'DKK' })) + assert_failure response + assert_equal response.error_code, 'INVALID_PROPERTY' + assert_equal response.message, 'authorize.currency' + end + + def test_successful_capture + authorize_response = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize_response + + response = @gateway.capture(@amount, authorize_response.authorization, @options) + assert_success response + assert_equal response.authorization, nil + end + + def test_successful_refund + purchase_response = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase_response + + response = @gateway.refund(@amount, purchase_response.authorization, @options) + assert_success response + assert_include response.params, 'transactionId' + end + + def test_successful_capture_with_less_authorized_amount_and_refund + authorize_response = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize_response + + capture_response = @gateway.capture(@amount - 100, authorize_response.authorization, @options) + assert_success capture_response + + response = @gateway.refund(@amount - 200, authorize_response.authorization, @options) + assert_success response + end + + def test_failed_partial_capture_already_captured + authorize_response = @gateway.authorize(2500, @credit_card, @options) + assert_success authorize_response + + capture_response = @gateway.capture(100, authorize_response.authorization, @options) + assert_success capture_response + + response = @gateway.capture(100, authorize_response.authorization, @options) + assert_failure response + assert_equal response.error_code, 'INVALID_TRANSACTION_STATUS' + assert_equal response.message, 'already settled' + end + + def test_failed_partial_capture_refund_refund_exceed_captured + authorize_response = @gateway.authorize(200, @credit_card, @options) + assert_success authorize_response + + capture_response = @gateway.capture(100, authorize_response.authorization, @options) + assert_success capture_response + + response = @gateway.refund(200, authorize_response.authorization, @options) + assert_failure response + assert_equal response.error_code, 'INVALID_PROPERTY' + assert_equal response.message, 'credit.amount' + end + + def test_failed_consecutive_partial_refund_when_total_exceed_amount + purchase_response = @gateway.purchase(700, @credit_card, @options) + + assert_success purchase_response + + refund_response_1 = @gateway.refund(200, purchase_response.authorization, @options) + assert_success refund_response_1 + + refund_response_2 = @gateway.refund(200, purchase_response.authorization, @options) + assert_success refund_response_2 + + refund_response_3 = @gateway.refund(200, purchase_response.authorization, @options) + assert_success refund_response_3 + + refund_response_4 = @gateway.refund(200, purchase_response.authorization, @options) + assert_failure refund_response_4 + assert_equal refund_response_4.error_code, 'INVALID_PROPERTY' + assert_equal refund_response_4.message, 'credit.amount' + end + + def test_failed_refund_not_settle_transaction + purchase_response = @gateway.authorize(@amount, @credit_card, @options) + assert_success purchase_response + + response = @gateway.refund(@amount, purchase_response.authorization, @options) + assert_failure response + assert_equal response.error_code, 'INVALID_TRANSACTION_STATUS' + assert_equal response.message, 'the transaction cannot be credited' + end + + def test_successful_void + authorize_response = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize_response + + response = @gateway.void(authorize_response.authorization, @options) + assert_success response + + assert_equal response.authorization, nil + end + + def test_failed_void_because_captured_transaction + omit("the transaction could take about 20 minutes to + pass from settle to transmited, use a previos + transaction acutually transmited and comment this + omition") + + # this is a previos transmited transaction, if the test fail use another, check dashboard to confirm it. + previous_authorization = '240417191339383491|339523493' + response = @gateway.void(previous_authorization, @options) + assert_failure response + assert_equal 'Action denied : Wrong transaction status', 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) + end + + def test_successful_purchase_with_billing_address + response = @gateway.purchase(@amount, @credit_card, @options.merge({ billing_address: @billing_address })) + + assert_success response + end + + def test_successful_purchase_with_network_token + response = @gateway.purchase(@amount, @nt_credit_card, @options) + + assert_success response + end + + def test_successful_purchase_with_apple_pay + response = @gateway.purchase(@amount, @apple_pay_card, @options) + + assert_success response + end + + def test_successful_authorize_with_google_pay + response = @gateway.authorize(@amount, @google_pay_card, @options) + assert_success response + end + + def test_successful_void_with_google_pay + authorize_response = @gateway.authorize(@amount, @google_pay_card, @options) + assert_success authorize_response + + response = @gateway.void(authorize_response.authorization, @options) + assert_success response + end + + def test_successful_purchase_with_3ds + response = @gateway.purchase(@amount, @credit_card_frictionless, @options.merge(@three_d_secure)) + assert_success response + end + + def test_failed_purchase_with_3ds + @three_d_secure[:three_d_secure][:cavv] = '\/\/\/\/8=' + response = @gateway.purchase(@amount, @credit_card_frictionless, @options.merge(@three_d_secure)) + assert_failure response + assert_equal response.error_code, 'INVALID_PROPERTY' + assert_equal response.message, 'cavv format is invalid. make sure that the value is base64 encoded and has a proper length.' + end +end diff --git a/test/remote/gateways/remote_decidir_plus_test.rb b/test/remote/gateways/remote_decidir_plus_test.rb index 0f36584dab5..5a27ae05fc8 100644 --- a/test/remote/gateways/remote_decidir_plus_test.rb +++ b/test/remote/gateways/remote_decidir_plus_test.rb @@ -160,7 +160,7 @@ def test_successful_verify def test_failed_verify assert response = @gateway_auth.verify(@declined_card, @options) assert_failure response - assert_equal 'missing: fraud_detection', response.message + assert_equal '10734: Fraud Detection Data is required', response.message end def test_successful_store @@ -217,7 +217,7 @@ def test_successful_purchase_with_fraud_detection response = @gateway_purchase.purchase(@amount, payment_reference, options) assert_success response - assert_equal({ 'status' => nil }, response.params['fraud_detection']) + assert_equal({ 'send_to_cs' => false, 'status' => nil }, response.params['fraud_detection']) end def test_successful_purchase_with_card_brand diff --git a/test/remote/gateways/remote_decidir_test.rb b/test/remote/gateways/remote_decidir_test.rb index 7e590f2fd00..6f91f22778c 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. # @@ -145,6 +171,18 @@ def test_successful_purchase_with_sub_payments assert_equal 'approved', response.message end + def test_successful_purchase_with_customer_object + customer_options = { + customer_id: 'John', + customer_email: 'decidir@decidir.com' + } + + assert response = @gateway_for_purchase.purchase(@amount, @credit_card, @options.merge(customer_options)) + assert_success response + + assert_equal 'approved', response.message + end + def test_failed_purchase_with_bad_csmdds options = { fraud_detection: { @@ -169,9 +207,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 +239,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 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 3c9624cb0a7..266c7b4e2ed 100644 --- a/test/remote/gateways/remote_ebanx_test.rb +++ b/test/remote/gateways/remote_ebanx_test.rb @@ -24,8 +24,12 @@ def setup metadata_2: 'test2' }, tags: EbanxGateway::TAGS, - soft_descriptor: 'ActiveMerchant' + soft_descriptor: 'ActiveMerchant', + email: 'neymar@test.com' } + + @hiper_card = credit_card('6062825624254001') + @elo_card = credit_card('6362970000457013') end def test_successful_purchase @@ -34,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, @@ -112,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 @@ -194,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 @@ -256,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_elavon_test.rb b/test/remote/gateways/remote_elavon_test.rb index f4c4356b404..6ad3e3c6097 100644 --- a/test/remote/gateways/remote_elavon_test.rb +++ b/test/remote/gateways/remote_elavon_test.rb @@ -401,11 +401,11 @@ def test_successful_purchase_with_custom_fields end def test_failed_purchase_with_multi_currency_terminal_setting_disabled - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'USD', multi_currency: true)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'ZAR', multi_currency: true)) assert_failure response assert response.test? - assert_equal 'Transaction currency is not allowed for this terminal. Your terminal must be setup with Multi currency', response.message + assert_equal 'The transaction currency sent is not supported', response.message assert response.authorization end @@ -429,7 +429,7 @@ def test_successful_purchase_with_multi_currency_transaction_setting end def test_successful_purchase_with_level_3_fields - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(level_3_data: @level_3_data)) + assert response = @gateway.purchase(500, @credit_card, @options.merge(level_3_data: @level_3_data)) assert_success response assert_equal 'APPROVAL', response.message @@ -445,7 +445,7 @@ def test_successful_purchase_with_shipping_address end def test_successful_purchase_with_shipping_address_and_l3 - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(shipping_address: @shipping_address).merge(level_3_data: @level_3_data)) + assert response = @gateway.purchase(500, @credit_card, @options.merge(shipping_address: @shipping_address).merge(level_3_data: @level_3_data)) assert_success response assert_equal 'APPROVAL', response.message diff --git a/test/remote/gateways/remote_element_test.rb b/test/remote/gateways/remote_element_test.rb index 7c90ff55646..3a9f2dbc42f 100644 --- a/test/remote/gateways/remote_element_test.rb +++ b/test/remote/gateways/remote_element_test.rb @@ -8,12 +8,14 @@ def setup @credit_card = credit_card('4000100011112224') @check = check @options = { - order_id: '1', - billing_address: address, - description: 'Store Purchase' + order_id: '2', + billing_address: address.merge(zip: '87654'), + description: 'Store Purchase', + duplicate_override_flag: 'true' } - @google_pay_network_token = network_tokenization_credit_card('4444333322221111', + @google_pay_network_token = network_tokenization_credit_card( + '4000100011112224', month: '01', year: Time.new.year + 2, first_name: 'Jane', @@ -22,9 +24,11 @@ def setup payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', eci: '05', transaction_id: '123456789', - source: :google_pay) + source: :google_pay + ) - @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', @@ -33,19 +37,20 @@ def setup payment_cryptogram: 'CeABBJQ1AgAAAAAgJDUCAAAAAAA=', eci: '07', transaction_id: 'abc123', - source: :apple_pay) + source: :apple_pay + ) end def test_successful_purchase response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 'Approved', response.message - assert_match %r{Street address and postal code do not match}, response.avs_result['message'] + assert_match %r{Street address and 5-digit postal code match.}, response.avs_result['message'] assert_match %r{CVV matches}, response.cvv_result['message'] end def test_failed_purchase - @amount = 20 + @amount = 51 response = @gateway.purchase(@amount, @credit_card, @options) assert_failure response assert_equal 'Declined', response.message @@ -115,19 +120,65 @@ def test_successful_purchase_with_duplicate_check_disable_flag end def test_successful_purchase_with_duplicate_override_flag - response = @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_override_flag: true)) - assert_success response - assert_equal 'Approved', response.message + options = { + order_id: '2', + billing_address: address.merge(zip: '87654'), + description: 'Store Purchase' + } - response = @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_override_flag: false)) + response = @gateway.purchase(@amount, @credit_card, options.merge(duplicate_override_flag: true)) assert_success response assert_equal 'Approved', response.message - response = @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_overrride_flag: 'true')) + response = @gateway.purchase(@amount, @credit_card, options.merge(duplicate_override_flag: 'true')) assert_success response assert_equal 'Approved', response.message - response = @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_override_flag: 'xxx')) + # Due to the way these new creds are configured, they fail on duplicate transactions. + # We expect failures if duplicate_override_flag: false + response = @gateway.purchase(@amount, @credit_card, options.merge(duplicate_override_flag: false)) + assert_failure response + assert_equal 'Duplicate', response.message + + response = @gateway.purchase(@amount, @credit_card, options.merge(duplicate_override_flag: 'xxx')) + assert_failure response + assert_equal 'Duplicate', response.message + end + + def test_successful_purchase_with_lodging_and_all_other_fields + lodging_options = { + order_id: '2', + billing_address: address.merge(zip: '87654'), + description: 'Store Purchase', + duplicate_override_flag: 'true', + lodging: { + agreement_number: SecureRandom.hex(12), + check_in_date: 20250910, + check_out_date: 20250915, + room_amount: 1000, + room_tax: 0, + no_show_indicator: 0, + duration: 5, + customer_name: 'francois dubois', + client_code: 'Default', + extra_charges_detail: '01', + extra_charges_amounts: 'Default', + prestigious_property_code: 'DollarLimit500', + special_program_code: 'Sale', + charge_type: 'Restaurant' + }, + card_holder_present_code: 'ECommerce', + card_input_code: 'ManualKeyed', + card_present_code: 'NotPresent', + cvv_presence_code: 'NotProvided', + market_code: 'HotelLodging', + terminal_capability_code: 'KeyEntered', + terminal_environment_code: 'ECommerce', + terminal_type: 'ECommerce', + terminal_id: '0001', + ticket_number: 182726718192 + } + response = @gateway.purchase(@amount, @credit_card, lodging_options) assert_success response assert_equal 'Approved', response.message end @@ -162,11 +213,11 @@ def test_successful_authorize_and_capture assert capture = @gateway.capture(@amount, auth.authorization) assert_success capture - assert_equal 'Success', capture.message + assert_equal 'Approved', capture.message end def test_failed_authorize - @amount = 20 + @amount = 51 response = @gateway.authorize(@amount, @credit_card, @options) assert_failure response assert_equal 'Declined', response.message 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_fat_zebra_test.rb b/test/remote/gateways/remote_fat_zebra_test.rb index ff8afe7c584..17da0b1c94d 100644 --- a/test/remote/gateways/remote_fat_zebra_test.rb +++ b/test/remote/gateways/remote_fat_zebra_test.rb @@ -227,4 +227,34 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.number, transcript) assert_scrubbed(@credit_card.verification_value, transcript) end + + def test_successful_purchase_with_3DS + @options[:three_d_secure] = { + version: '2.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_failed_purchase_with_3DS + @options[:three_d_secure] = { + version: '3.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match(/version is not valid/, response.message) + end end diff --git a/test/remote/gateways/remote_first_pay_json_test.rb b/test/remote/gateways/remote_first_pay_json_test.rb new file mode 100644 index 00000000000..0ca84b502e7 --- /dev/null +++ b/test/remote/gateways/remote_first_pay_json_test.rb @@ -0,0 +1,162 @@ +require 'test_helper' + +class RemoteFirstPayJsonTest < Test::Unit::TestCase + def setup + @gateway = FirstPayGateway.new(fixtures(:first_pay_rest_json)) + + @amount = 100 + @credit_card = credit_card('4111111111111111') + @declined_card = credit_card('5130405452262903') + + @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=' + ) + + @options = { + order_id: SecureRandom.hex(24), + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_match 'APPROVED', response.message + end + + def test_failed_purchase + response = @gateway.purchase(99999999999, @credit_card, @options) + assert_failure response + assert_equal 'validationHasFailed', response.error_code + assert_match 'Amount exceed numeric limit of 9999999.99', response.message + end + + def test_successful_purchase_with_google_pay + response = @gateway.purchase(@amount, @google_pay, @options) + assert_success response + assert_match 'APPROVED', response.message + assert_equal 'Visa-GooglePay', response.params['data']['cardType'] + end + + def test_successful_purchase_with_apple_pay + response = @gateway.purchase(@amount, @apple_pay, @options) + assert_success response + assert_match 'APPROVED', response.message + assert_equal 'Visa-ApplePay', response.params['data']['cardType'] + end + + def test_failed_purchase_with_no_address + @options.delete(:billing_address) + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_equal 'validationHasFailed', response.error_code + assert_equal 'Name on credit card is required; Street is required.; City is required.; State is required.; Postal Code is required.', response.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 + end + + def test_failed_authorize + response = @gateway.authorize(99999999999, @credit_card, @options) + assert_failure response + end + + def test_failed_capture + response = @gateway.capture(@amount, '1234') + assert_failure response + end + + def test_successful_refund_for_authorize_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + + assert refund = @gateway.refund(@amount, capture.authorization) + assert_success refund + end + + def test_successful_refund_for_purchase + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + end + + def test_failed_refund + response = @gateway.refund(@amount, '1234') + assert_failure response + end + + def test_successful_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert void = @gateway.void(auth.authorization) + assert_success void + end + + def test_failed_void + response = @gateway.void('1') + assert_failure response + end + + def test_recurring_payment + @options.merge!({ + recurring: 'monthly', + recurring_start_date: (DateTime.now + 1.day).strftime('%m/%d/%Y'), + recurring_end_date: (DateTime.now + 1.month).strftime('%m/%d/%Y') + }) + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_match 'APPROVED', response.message + end + + def test_invalid_login + gateway = FirstPayGateway.new( + processor_id: '1234', + merchant_key: 'abcd' + ) + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal('isError', response.error_code) + end + + def test_transcript_scrubbing + @google_pay.verification_value = 789 + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @google_pay, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@google_pay.number, transcript) + assert_scrubbed(@google_pay.verification_value, transcript) + assert_scrubbed(@google_pay.payment_cryptogram, transcript) + assert_scrubbed(@gateway.options[:processor_id], transcript) + assert_scrubbed(@gateway.options[:merchant_key], transcript) + end +end 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_flex_charge_test.rb b/test/remote/gateways/remote_flex_charge_test.rb new file mode 100644 index 00000000000..fd2ce646c94 --- /dev/null +++ b/test/remote/gateways/remote_flex_charge_test.rb @@ -0,0 +1,224 @@ +require 'timecop' +require 'test_helper' + +class RemoteFlexChargeTest < Test::Unit::TestCase + def setup + @gateway = FlexChargeGateway.new(fixtures(:flex_charge)) + + @amount = 100 + @credit_card_cit = credit_card('4111111111111111', verification_value: '999', first_name: 'Cure', last_name: 'Tester') + @credit_card_mit = credit_card('4000002760003184') + @declined_card = credit_card('4000300011112220') + + @options = { + is_mit: true, + is_recurring: false, + mit_expiry_date_utc: (Time.now + 1.day).getutc.iso8601, + description: 'MyShoesStore', + is_declined: true, + order_id: SecureRandom.uuid, + idempotency_key: SecureRandom.uuid, + card_not_present: false, + email: 'test@gmail.com', + response_code: '100', + response_code_source: 'nmi', + avs_result_code: '200', + cvv_result_code: '111', + cavv_result_code: '111', + timezone_utc_offset: '-5', + billing_address: address.merge(name: 'Cure Tester') + } + + @cit_options = @options.merge( + is_mit: false, + phone: '+99.2001a/+99.2001b' + ) + end + + def test_successful_purchase_with_three_ds_global + @options[:three_d_secure] = { + version: '2.1.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + xid: 'MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=', + cavv_algorithm: 'AAABCSIIAAAAAAACcwgAEMCoNh=', + enrolled: 'Y', + authentication_response_status: 'Y' + } + + response = @gateway.purchase(@amount, @credit_card_cit, @options) + assert_success response + assert_match 'SUBMITTED', response.message + end + + def test_setting_access_token_when_no_present + assert_nil @gateway.options[:access_token] + + @gateway.send(:fetch_access_token) + + assert_not_nil @gateway.options[:access_token] + assert_not_nil @gateway.options[:token_expires] + end + + def test_successful_access_token_generation_and_use + @gateway.send(:fetch_access_token) + + second_purchase = @gateway.purchase(@amount, @credit_card_cit, @cit_options) + + assert_success second_purchase + assert_kind_of MultiResponse, second_purchase + assert_equal 1, second_purchase.responses.size + assert_equal @gateway.options[:access_token], second_purchase.params[:access_token] + end + + def test_successful_purchase_with_an_expired_access_token + initial_access_token = @gateway.options[:access_token] = SecureRandom.alphanumeric(10) + initial_expires = @gateway.options[:token_expires] = DateTime.now.strftime('%Q').to_i + + Timecop.freeze(DateTime.now + 10.minutes) do + second_purchase = @gateway.purchase(@amount, @credit_card_cit, @cit_options) + assert_success second_purchase + + assert_equal 2, second_purchase.responses.size + assert_not_equal initial_access_token, @gateway.options[:access_token] + assert_not_equal initial_expires, @gateway.options[:token_expires] + + assert_not_nil second_purchase.params[:access_token] + assert_not_nil second_purchase.params[:token_expires] + + assert_nil second_purchase.responses.first.params[:access_token] + end + end + + def test_should_reset_access_token_when_401_error + @gateway.options[:access_token] = SecureRandom.alphanumeric(10) + @gateway.options[:token_expires] = DateTime.now.strftime('%Q').to_i + 15000 + + response = @gateway.purchase(@amount, @credit_card_cit, @cit_options) + + assert_equal '', response.params['access_token'] + end + + def test_successful_purchase_cit_challenge_purchase + set_credentials! + response = @gateway.purchase(@amount, @credit_card_cit, @cit_options) + assert_success response + assert_equal 'CHALLENGE', response.message + end + + def test_successful_purchase_mit + set_credentials! + response = @gateway.purchase(@amount, @credit_card_mit, @options) + assert_success response + assert_equal 'SUBMITTED', response.message + end + + def test_failed_purchase + set_credentials! + response = @gateway.purchase(@amount, @credit_card_cit, billing_address: address) + assert_failure response + assert_equal nil, response.error_code + assert_not_nil response.params['TraceId'] + end + + def test_failed_cit_declined_purchase + set_credentials! + response = @gateway.purchase(@amount, @credit_card_cit, @cit_options.except(:phone)) + assert_failure response + assert_equal 'DECLINED', response.error_code + end + + def test_successful_refund + set_credentials! + purchase = @gateway.purchase(@amount, @credit_card_mit, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal 'DECLINED', refund.message + end + + def test_partial_refund + omit('Partial refunds requires to raise some limits on merchant account') + set_credentials! + purchase = @gateway.purchase(100, @credit_card_cit, @options) + assert_success purchase + + assert refund = @gateway.refund(90, purchase.authorization) + assert_success refund + assert_equal 'DECLINED', refund.message + end + + def test_failed_fetch_access_token + error = assert_raises(ActiveMerchant::OAuthResponseError) do + gateway = FlexChargeGateway.new( + app_key: 'SOMECREDENTIAL', + app_secret: 'SOMECREDENTIAL', + site_id: 'SOMECREDENTIAL', + mid: 'SOMECREDENTIAL' + ) + gateway.send :fetch_access_token + end + + assert_match(/400/, error.message) + end + + def test_successful_purchase_with_token + set_credentials! + store = @gateway.store(@credit_card_cit, {}) + assert_success store + + response = @gateway.purchase(@amount, store.authorization, @options) + assert_success response + end + + def test_successful_inquire_request + set_credentials! + response = @gateway.inquire('abe573e3-7567-4cc6-a7a4-02766dbd881a', {}) + assert_success response + end + + def test_unsuccessful_inquire_request + set_credentials! + response = @gateway.inquire(SecureRandom.uuid, {}) + assert_failure response + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card_cit, @cit_options) + end + + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@credit_card_cit.number, transcript) + assert_scrubbed(@credit_card_cit.verification_value, transcript) + assert_scrubbed(@gateway.options[:access_token], transcript) + assert_scrubbed(@gateway.options[:app_key], transcript) + assert_scrubbed(@gateway.options[:app_secret], transcript) + assert_scrubbed(@gateway.options[:site_id], transcript) + assert_scrubbed(@gateway.options[:mid], transcript) + end + + private + + def set_credentials! + if FlexChargeCredentials.instance.access_token.nil? + @gateway.send :fetch_access_token + FlexChargeCredentials.instance.access_token = @gateway.options[:access_token] + FlexChargeCredentials.instance.token_expires = @gateway.options[:token_expires] + end + + @gateway.options[:access_token] = FlexChargeCredentials.instance.access_token + @gateway.options[:token_expires] = FlexChargeCredentials.instance.token_expires + end +end + +# A simple singleton so access-token and expires can +# be shared among several tests +class FlexChargeCredentials + include Singleton + + attr_accessor :access_token, :token_expires +end diff --git a/test/remote/gateways/remote_global_collect_test.rb b/test/remote/gateways/remote_global_collect_test.rb index 9c2553700bf..e325c35e5df 100644 --- a/test/remote/gateways/remote_global_collect_test.rb +++ b/test/remote/gateways/remote_global_collect_test.rb @@ -6,32 +6,31 @@ 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', + @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) + source: :apple_pay + ) - @google_pay = network_tokenization_credit_card('4567350000427977', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - month: '01', - year: Time.new.year + 2, + @google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ source: :google_pay, - transaction_id: '123456789', - eci: '05') - - @google_pay_pan_only = credit_card('4567350000427977', - month: '01', - year: Time.new.year + 2) + payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}" + }) @accepted_amount = 4005 @rejected_amount = 2997 @@ -63,6 +62,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) @@ -87,29 +94,36 @@ def test_successful_purchase_with_apple_pay 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) + def test_successful_authorize_with_apple_pay + options = @preprod_options.merge(requires_approval: false, currency: 'USD') + response = @gateway_preprod.authorize(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_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) - + def test_successful_purchase_with_apple_pay_ogone_direct + options = @preprod_options.merge(requires_approval: false, currency: 'EUR') + response = @gateway_direct.purchase(100, @apple_pay, options) assert_success response assert_equal 'Succeeded', response.message - assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status'] + assert_equal 'PENDING_CAPTURE', 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) + def test_successful_authorize_and_capture_with_apple_pay_ogone_direct + options = @preprod_options.merge(requires_approval: false, currency: 'EUR') + auth = @gateway_direct.authorize(100, @apple_pay, options) + assert_success auth + + assert capture = @gateway_direct.capture(@amount, auth.authorization, @options) + assert_success capture + assert_equal 'Succeeded', capture.message + end + def test_failed_purchase_with_google_pay + options = @preprod_options.merge(requires_approval: false) + response = @gateway_direct.purchase(4500, @google_pay, options) assert_failure response - assert_equal 'order.customer.merchantCustomerId is missing for UCOF', response.message end def test_successful_purchase_with_fraud_fields @@ -145,8 +159,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 @@ -159,20 +173,32 @@ 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 assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status'] end + def test_successful_authorize_with_moto_exemption + options = @options.merge(three_ds_exemption_type: 'moto') + + response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + end + def test_successful_authorize_via_normalized_3ds2_fields options = @options.merge( three_d_secure: { @@ -193,6 +219,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: { @@ -205,6 +281,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', @@ -321,6 +398,14 @@ 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 @@ -335,7 +420,7 @@ def test_successful_purchase_with_payment_product_id assert_equal 135, response.params['payment']['paymentOutput']['cardPaymentMethodSpecificOutput']['paymentProductId'] end - def test_successful_purchase_with_truncated_address + def test_successful_purchase_with_truncated_split_address response = @gateway.purchase(@amount, @credit_card, @long_address) assert_success response assert_equal 'Succeeded', response.message @@ -347,6 +432,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 @@ -368,13 +459,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 @@ -416,6 +512,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 @@ -450,12 +555,24 @@ def test_successful_verify 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) @@ -473,16 +590,6 @@ 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 diff --git a/test/remote/gateways/remote_hi_pay_test.rb b/test/remote/gateways/remote_hi_pay_test.rb new file mode 100644 index 00000000000..0d0af9025e0 --- /dev/null +++ b/test/remote/gateways/remote_hi_pay_test.rb @@ -0,0 +1,241 @@ +require 'test_helper' + +class RemoteHiPayTest < Test::Unit::TestCase + def setup + @gateway = HiPayGateway.new(fixtures(:hi_pay)) + @bad_gateway = HiPayGateway.new(username: 'bad', password: 'password') + + @amount = 500 + @credit_card = credit_card('4111111111111111', verification_value: '514', first_name: 'John', last_name: 'Smith', month: 12, year: 2025) + @bad_credit_card = credit_card('4150551403657424') + @master_credit_card = credit_card('5399999999999999') + @challenge_credit_card = credit_card('4242424242424242') + + @options = { + order_id: "Sp_ORDER_#{SecureRandom.random_number(1000000000)}", + description: 'An authorize', + email: 'john.smith@test.com' + } + + @billing_address = address + + @execute_threed = { + execute_threed: true, + redirect_url: 'http://www.example.com/redirect', + callback_url: 'http://www.example.com/callback', + 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_authorize + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_success response + assert_equal response.message, 'Authorized' + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Captured', response.message + + assert_kind_of MultiResponse, response + assert_equal 2, response.responses.size + end + + def test_successful_purchase_with_3ds + response = @gateway.purchase(@amount, @challenge_credit_card, @options.merge(@billing_address).merge(@execute_threed)) + assert_success response + assert_equal 'Authentication requested', response.message + assert_match %r{stage-secure-gateway.hipay-tpp.com\/gateway\/forward\/\w+}, response.params['forwardUrl'] + + assert_kind_of MultiResponse, response + assert_equal 2, response.responses.size + end + + def test_successful_purchase_with_mastercard + response = @gateway.purchase(@amount, @master_credit_card, @options) + assert_success response + assert_equal 'Captured', response.message + + assert_kind_of MultiResponse, response + assert_equal 2, response.responses.size + end + + def test_failed_purchase_due_failed_tokenization + response = @bad_gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'Incorrect Credentials _ Username and/or password is incorrect', response.message + assert_equal '1000001', response.error_code + + assert_kind_of MultiResponse, response + # Failed in tokenization step + assert_equal 1, response.responses.size + end + + def test_failed_purchase_due_authorization_refused + response = @gateway.purchase(@amount, @bad_credit_card, @options) + assert_failure response + assert_equal 'Authorization Refused', response.message + assert_equal '1010201', response.error_code + assert_equal 'Invalid Parameter', response.params['reason']['message'] + + assert_kind_of MultiResponse, response + # Complete tokenization, failed in the purhcase step + assert_equal 2, response.responses.size + end + + def test_successful_purchase_with_billing_address + response = @gateway.purchase(@amount, @credit_card, @options.merge({ billing_address: @billing_address })) + + assert_success response + end + + def test_successful_capture + authorize_response = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize_response + + response = @gateway.capture(@amount, authorize_response.authorization, @options) + assert_success response + assert_equal 'Captured', response.message + assert_equal authorize_response.authorization, response.authorization + end + + def test_successful_authorize_with_store + store_response = @gateway.store(@credit_card, @options) + assert_nil store_response.message + assert_success store_response + assert_not_empty store_response.authorization + + response = @gateway.authorize(@amount, store_response.authorization, @options) + assert_success response + end + + def test_successful_multiple_purchases_with_single_store + store_response = @gateway.store(@credit_card, @options) + assert_success store_response + + response1 = @gateway.purchase(@amount, store_response.authorization, @options) + assert_success response1 + + @options[:order_id] = "Sp_ORDER_2_#{SecureRandom.random_number(1000000000)}" + + response2 = @gateway.purchase(@amount, store_response.authorization, @options) + assert_success response2 + end + + def test_successful_unstore + store_response = @gateway.store(@credit_card, @options) + assert_success store_response + + response = @gateway.unstore(store_response.authorization, @options) + assert_success response + end + + def test_failed_purchase_after_unstore_payment_method + store_response = @gateway.store(@credit_card, @options) + assert_success store_response + + purchase_response = @gateway.purchase(@amount, store_response.authorization, @options) + assert_success purchase_response + + unstore_response = @gateway.unstore(store_response.authorization, @options) + assert_success unstore_response + + response = @gateway.purchase( + @amount, + store_response.authorization, + @options.merge( + { + order_id: "Sp_UNSTORE_#{SecureRandom.random_number(1000000000)}" + } + ) + ) + assert_failure response + assert_equal 'Unknown Token', response.message + assert_equal '3040001', response.error_code + end + + def test_successful_refund + purchase_response = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase_response + + response = @gateway.refund(@amount, purchase_response.authorization, @options) + assert_success response + assert_equal 'Refund Requested', response.message + assert_include response.params['authorizedAmount'], '5.00' + assert_include response.params['capturedAmount'], '5.00' + assert_include response.params['refundedAmount'], '5.00' + end + + def test_successful_partial_capture_refund + authorize_response = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize_response + assert_include authorize_response.params['authorizedAmount'], '5.00' + assert_include authorize_response.params['capturedAmount'], '0.00' + assert_equal authorize_response.params['refundedAmount'], '0.00' + + capture_response = @gateway.capture(@amount - 100, authorize_response.authorization, @options) + assert_success capture_response + assert_equal authorize_response.authorization, capture_response.authorization + assert_include capture_response.params['authorizedAmount'], '5.00' + assert_include capture_response.params['capturedAmount'], '4.00' + assert_equal capture_response.params['refundedAmount'], '0.00' + + response = @gateway.refund(@amount - 200, capture_response.authorization, @options) + assert_success response + assert_include response.params['authorizedAmount'], '5.00' + assert_include response.params['capturedAmount'], '4.00' + assert_include response.params['refundedAmount'], '3.00' + end + + def test_failed_refund_because_auth_no_captured + authorize_response = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize_response + + response = @gateway.refund(@amount, authorize_response.authorization, @options) + assert_failure response + assert_equal 'Operation Not Permitted : transaction not captured', response.message + end + + def test_successful_void + authorize_response = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize_response + + response = @gateway.void(authorize_response.authorization, @options) + assert_success response + assert_equal 'Authorization Cancellation requested', response.message + end + + def test_failed_void_because_captured_transaction + purchase_response = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase_response + + response = @gateway.void(purchase_response.authorization, @options) + assert_failure response + assert_equal 'Action denied : Wrong transaction status', 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) + end +end 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 8c797606e97..95ca04930e8 100644 --- a/test/remote/gateways/remote_ipg_test.rb +++ b/test/remote/gateways/remote_ipg_test.rb @@ -3,11 +3,11 @@ class RemoteIpgTest < Test::Unit::TestCase def setup @gateway = IpgGateway.new(fixtures(:ipg)) - + @gateway_ma = IpgGateway.new(fixtures(:ipg_ma).merge({ store_id: nil })) @amount = 100 - @credit_card = credit_card('5165850000000008', brand: 'mastercard', verification_value: '530', month: '12', year: '2022') + @credit_card = credit_card('5165850000000008', brand: 'mastercard', 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,15 +96,15 @@ 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 + response = @gateway.purchase(@amount, @visa_card, @options.merge({ store_id: '1234' })) + + assert_failure response + assert 'MerchantException', response.params['faultstring'] end def test_successful_authorize_and_capture @@ -121,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 @@ -159,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 @@ -172,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 @@ -184,4 +184,16 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.number, transcript) assert_scrubbed(@credit_card.verification_value, transcript) end + + def test_successful_purchase_with_ma_credentials + response = @gateway_ma.purchase(@amount, @credit_card, @options.merge({ store_id: fixtures(:ipg_ma)[:store_id] })) + assert_success response + assert_equal 'APPROVED', response.message + end + + def test_failed_purchase_without_store_id + assert_raises(ArgumentError) do + @gateway_ma.purchase(@amount, @credit_card, @options) + end + end end diff --git a/test/remote/gateways/remote_kushki_test.rb b/test/remote/gateways/remote_kushki_test.rb index 1fb1ad41a98..d1c71647e15 100644 --- a/test/remote/gateways/remote_kushki_test.rb +++ b/test/remote/gateways/remote_kushki_test.rb @@ -3,6 +3,7 @@ class RemoteKushkiTest < Test::Unit::TestCase def setup @gateway = KushkiGateway.new(fixtures(:kushki)) + @gateway_partial_refund = KushkiGateway.new(fixtures(:kushki_partial)) @amount = 100 @credit_card = credit_card('4000100011112224', verification_value: '777') @declined_card = credit_card('4000300011112220') @@ -15,6 +16,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 +44,27 @@ 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, + product_details: [ + { + id: 'test1', + title: 'tester1', + price: 10, + sku: 'abcde', + quantity: 1 + }, + { + id: 'test2', + title: 'tester2', + price: 5, + sku: 'edcba', + quantity: 2 + } + ] } amount = 100 * ( @@ -133,8 +161,14 @@ 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' }) + response = @gateway_partial_refund.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 @@ -162,6 +196,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_partial_refund.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_partial_refund.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_partial_refund.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_partial_refund.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_partial_refund.authorize(@amount, credit_card, options) + end + end + def test_successful_capture auth = @gateway.authorize(@amount, @credit_card) assert_success auth @@ -204,6 +333,26 @@ def test_failed_refund assert_equal 'Missing Authentication Token', refund.message end + # partial refunds are only available in Colombia, Chile, Mexico and Peru + def test_partial_refund + options = { + currency: 'PEN', + full_response: 'v2' + } + purchase = @gateway_partial_refund.purchase(500, @credit_card, options) + assert_success purchase + + refund_options = { + currency: 'PEN', + partial_refund: true, + full_response: 'v2' + } + + assert refund = @gateway_partial_refund.refund(250, purchase.authorization, refund_options) + assert_success refund + assert_equal 'Succeeded', refund.message + end + def test_successful_void purchase = @gateway.purchase(@amount, @credit_card) assert_success purchase 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 465f9b1cc4b..c16c628ee2f 100644 --- a/test/remote/gateways/remote_litle_test.rb +++ b/test/remote/gateways/remote_litle_test.rb @@ -86,12 +86,15 @@ def setup name: 'John Smith', routing_number: '011075150', account_number: '1099999999', - account_type: 'checking' + account_type: nil, + account_holder_type: 'checking' ) @store_check = check( routing_number: '011100012', account_number: '1099999998' ) + + @declined_card = credit_card('4488282659650110', first_name: nil, last_name: 'REFUSED') end def test_successful_authorization @@ -143,14 +146,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 +174,8 @@ def test_unsuccessful_authorization zip: '03038', country: 'US' } - }) + } + ) assert_failure response assert_equal 'Insufficient Funds', response.message end @@ -231,7 +245,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 @@ -295,7 +309,7 @@ def test_successful_purchase_with_level_three_data_visa card_acceptor_tax_id: '361531321', line_items: [{ item_sequence_number: 1, - item_commodity_code: 300, + commodity_code: '041235', item_description: 'ramdom-object', product_code: 'TB123', quantity: 2, @@ -333,6 +347,7 @@ def test_successful_purchase_with_level_three_data_master customer_code: 'PO12345', card_acceptor_tax_id: '011234567', tax_amount: 50, + tax_included_in_total: true, line_items: [{ item_description: 'ramdom-object', product_code: 'TB123', @@ -370,7 +385,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', @@ -398,6 +413,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 @@ -422,7 +439,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) @@ -438,9 +455,10 @@ def test_authorize_and_capture_with_stored_credential_recurring network_transaction_id: network_transaction_id } ) + 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 @@ -617,6 +635,7 @@ def test_purchase_with_stored_credential_cit_card_on_file_non_ecommerce } ) assert auth = @gateway.purchase(4000, credit_card, used_options) + assert_success auth assert_equal 'Approved', auth.message end @@ -642,9 +661,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 @@ -710,15 +729,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 @@ -733,10 +752,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 @@ -750,11 +767,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 @@ -768,7 +785,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 @@ -780,7 +797,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 @@ -793,7 +810,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 b9a9c0d5f3e..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, 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_merchant_warrior_test.rb b/test/remote/gateways/remote_merchant_warrior_test.rb index 5cd61daff72..852e79ce380 100644 --- a/test/remote/gateways/remote_merchant_warrior_test.rb +++ b/test/remote/gateways/remote_merchant_warrior_test.rb @@ -60,7 +60,7 @@ def test_successful_purchase def test_failed_purchase assert purchase = @gateway.purchase(@success_amount, @expired_card, @options) - assert_match 'Card has expired', purchase.message + assert_match 'Transaction declined', purchase.message assert_failure purchase assert_not_nil purchase.params['transaction_id'] assert_equal purchase.params['transaction_id'], purchase.authorization @@ -192,4 +192,34 @@ def test_transcript_scrubbing_store assert_scrubbed(@gateway.options[:api_passphrase], transcript) assert_scrubbed(@gateway.options[:api_key], transcript) end + + def test_successful_purchase_with_three_ds + @options[:three_d_secure] = { + version: '2.2.0', + cavv: 'e1E3SN0xF1lDp9js723iASu3wrA=', + eci: '05', + xid: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } + + assert response = @gateway.purchase(@success_amount, @credit_card, @options) + assert_success response + assert_equal 'Transaction approved', response.message + end + + def test_successful_purchase_with_three_ds_transaction_id + @options[:three_d_secure] = { + version: '2.2.0', + cavv: 'e1E3SN0xF1lDp9js723iASu3wrA=', + eci: '05', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } + + assert response = @gateway.purchase(@success_amount, @credit_card, @options) + assert_success response + assert_equal 'Transaction approved', response.message + end end diff --git a/test/remote/gateways/remote_moneris_test.rb b/test/remote/gateways/remote_moneris_test.rb index 11f2d8af78a..4a6b6a2c842 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? @@ -56,6 +68,14 @@ def test_successful_first_purchase_with_credential_on_file assert_not_empty response.params['issuer_id'] end + def test_successful_first_purchase_with_cust_id + gateway = MonerisGateway.new(fixtures(:moneris)) + assert response = gateway.purchase(@amount, @credit_card, @options.merge(cust_id: 'test1234')) + assert_success response + assert_equal 'Approved', response.message + assert_false response.authorization.blank? + end + def test_successful_purchase_with_cof_enabled_and_no_cof_options gateway = MonerisGateway.new(fixtures(:moneris)) assert response = gateway.purchase(@amount, @credit_card, @options) @@ -104,38 +124,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 - @credit_card = network_tokenization_credit_card( - '4242424242424242', - payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', - verification_value: nil, - source: :google_pay - ) - assert response = @gateway.purchase(@amount, @credit_card, @options) + 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? @@ -185,7 +241,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', @@ -194,7 +252,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? @@ -203,7 +262,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', @@ -212,7 +273,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? @@ -225,7 +287,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', @@ -234,7 +298,8 @@ def test_failed_cavv_authorization three_ds_server_trans_id: 'e11d4985-8d25-40ed-99d6-c3803fe5e68f', ds_transaction_id: '12345' } - )) + ) + ) assert_failure response end @@ -327,8 +392,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 2eacfc42fa1..a3b5606fef1 100644 --- a/test/remote/gateways/remote_mundipagg_test.rb +++ b/test/remote/gateways/remote_mundipagg_test.rb @@ -26,26 +26,26 @@ def setup @submerchant_options = { submerchant: { - "merchant_category_code": '44444', - "payment_facilitator_code": '5555555', - "code": 'code2', - "name": 'Sub Tony Stark', - "document": '123456789', - "type": 'individual', - "phone": { - "country_code": '55', - "number": '000000000', - "area_code": '21' + merchant_category_code: '44444', + payment_facilitator_code: '5555555', + code: 'code2', + name: 'Sub Tony Stark', + document: '123456789', + type: 'individual', + phone: { + country_code: '55', + number: '000000000', + area_code: '21' }, - "address": { - "street": 'Malibu Point', - "number": '10880', - "complement": 'A', - "neighborhood": 'Central Malibu', - "city": 'Malibu', - "state": 'CA', - "country": 'US', - "zip_code": '24210-460' + address: { + street: 'Malibu Point', + number: '10880', + complement: 'A', + neighborhood: 'Central Malibu', + city: 'Malibu', + state: 'CA', + country: 'US', + zip_code: '24210-460' } } } 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..bd77940790b 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 @@ -366,6 +388,18 @@ def test_purchase_using_stored_credential_recurring_mit assert_success purchase end + def test_purchase_using_ntid_override_mit + initial_options = stored_credential_options(:cardholder, :recurring, :initial) + assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success purchase + assert network_transaction_id = purchase.params['transactionid'] + + @options[:network_transaction_id] = network_transaction_id + used_options = stored_credential_options(:merchant, :recurring) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + def test_purchase_using_stored_credential_installment_cit initial_options = stored_credential_options(:cardholder, :installment, :initial) assert purchase = @gateway.purchase(@amount, @credit_card, initial_options) diff --git a/test/remote/gateways/remote_ogone_test.rb b/test/remote/gateways/remote_ogone_test.rb index 800e635563a..202c17002f7 100644 --- a/test/remote/gateways/remote_ogone_test.rb +++ b/test/remote/gateways/remote_ogone_test.rb @@ -5,12 +5,16 @@ 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('4874970686672022', verification_value: '123') + @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], @@ -22,36 +26,36 @@ def setup @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' + 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 @@ -87,25 +91,35 @@ def test_successful_purchase_with_signature_encryptor_to_sha512 # 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_v1 - assert response = @gateway.purchase(@amount, @credit_card_d3d, @options.merge(d3d: true)) + 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_match 'mpi-v1', Base64.decode64(response.params['HTML_ANSWER']) + assert Base64.decode64(response.params['HTML_ANSWER']) end def test_successful_purchase_with_3d_secure_v2 - assert response = @gateway.purchase(@amount, @credit_card_d3d_2_challenge, @options_browser_info.merge(d3d: true)) + 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_match 'mpi-v2', Base64.decode64(response.params['HTML_ANSWER']) + assert Base64.decode64(response.params['HTML_ANSWER']) end def test_successful_purchase_with_3d_secure_v2_frictionless - assert response = @gateway.purchase(@amount, @credit_card_d3d_2_frictionless, @options_browser_info.merge(d3d: true)) + 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'] @@ -115,27 +129,27 @@ def test_successful_purchase_with_3d_secure_v2_frictionless def test_successful_purchase_with_3d_secure_v2_recomended_parameters options = @options.merge(@options_browser_info) - assert response = @gateway.authorize(@amount, @credit_card_d3d_2_challenge, options.merge(d3d: true)) + 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_match 'mpi-v2', Base64.decode64(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.authorize(@amount, @credit_card_d3d_2_challenge, options.merge(d3d: true)) + 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_match 'mpi-v2', Base64.decode64(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.purchase(@amount, @credit_card_d3d_2_challenge, @options_browser_info.merge(d3d: true)) + 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' @@ -145,20 +159,20 @@ def test_unsuccessful_purchase_with_3d_secure_v2 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 @@ -166,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 @@ -175,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 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 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 @@ -274,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 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_orbital_test.rb b/test/remote/gateways/remote_orbital_test.rb index 5152ab4c338..b923774c987 100644 --- a/test/remote/gateways/remote_orbital_test.rb +++ b/test/remote/gateways/remote_orbital_test.rb @@ -1,4 +1,4 @@ -require 'test_helper.rb' +require 'test_helper' class RemoteOrbitalGatewayTest < Test::Unit::TestCase def setup @@ -6,10 +6,11 @@ def setup @gateway = ActiveMerchant::Billing::OrbitalGateway.new(fixtures(:orbital_gateway)) @echeck_gateway = ActiveMerchant::Billing::OrbitalGateway.new(fixtures(:orbital_asv_aoa_gateway)) @three_ds_gateway = ActiveMerchant::Billing::OrbitalGateway.new(fixtures(:orbital_3ds_gateway)) - + @tpv_orbital_gateway = ActiveMerchant::Billing::OrbitalGateway.new(fixtures(:orbital_tpv_gateway)) @amount = 100 @google_pay_amount = 10000 @credit_card = credit_card('4556761029983886') + @mastercard_card_tpv = credit_card('2521000000000006') @declined_card = credit_card('4000300011112220') # Electronic Check object with test credentials of saving account @echeck = check(account_number: '072403004', account_type: 'savings', routing_number: '072403004') @@ -50,7 +51,15 @@ def setup address2: address[:address2], city: address[:city], state: address[:state], - zip: address[:zip] + zip: address[:zip], + requestor_name: 'ArtVandelay123', + total_tax_amount: '75', + national_tax: '625', + pst_tax_reg_number: '8675309', + customer_vat_reg_number: '1234567890', + merchant_vat_reg_number: '987654321', + commodity_code: 'SUMM', + local_tax_rate: '6250' } @level_3_options_visa = { @@ -60,7 +69,11 @@ def setup dest_country: 'USA', discount_amount: 1, vat_tax: 1, - vat_rate: 25 + vat_rate: 25, + invoice_discount_treatment: 1, + tax_treatment: 1, + ship_vat_rate: 10, + unique_vat_invoice_ref: 'ABC123' } @level_2_options_master = { @@ -195,11 +208,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 +264,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 +278,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 @@ -823,6 +842,98 @@ def test_successful_verify assert_equal 'No reason to decline', response.message end + def test_successful_store + response = @tpv_orbital_gateway.store(@mastercard_card_tpv, @options) + assert_success response + assert_false response.params['safetech_token'].blank? + end + + def test_successful_purchase_stored_token + store = @tpv_orbital_gateway.store(@credit_card, @options) + assert_success store + response = @tpv_orbital_gateway.purchase(@amount, store.authorization, @options.merge(card_brand: 'VI')) + assert_success response + assert_equal response.params['card_brand'], 'VI' + end + + def test_successful_authorize_stored_token + store = @tpv_orbital_gateway.store(@credit_card, @options) + assert_success store + auth = @tpv_orbital_gateway.authorize(29, store.authorization, @options.merge(card_brand: 'VI')) + assert_success auth + end + + def test_successful_authorize_stored_token_mastercard + store = @tpv_orbital_gateway.store(@mastercard_card_tpv, @options) + assert_success store + response = @tpv_orbital_gateway.authorize(29, store.authorization, @options.merge(card_brand: 'MC')) + assert_success response + assert_equal response.params['card_brand'], 'MC' + end + + def test_failed_authorize_and_capture + store = @tpv_orbital_gateway.store(@credit_card, @options) + assert_success store + authorization = store.authorization.split(';').values_at(2).first + response = @tpv_orbital_gateway.capture(39, authorization, @options.merge(card_brand: 'VI')) + assert_failure response + assert_equal response.params['status_msg'], "The LIDM you supplied (#{authorization}) does not match with any existing transaction" + end + + def test_successful_authorize_and_capture_with_stored_token + store = @tpv_orbital_gateway.store(@mastercard_card_tpv, @options) + assert_success store + auth = @tpv_orbital_gateway.authorize(28, store.authorization, @options.merge(card_brand: 'MC')) + assert_success auth + assert_equal auth.params['card_brand'], 'MC' + response = @tpv_orbital_gateway.capture(28, auth.authorization, @options.merge(card_brand: 'MC')) + assert_success response + end + + def test_successful_authorize_with_stored_token_and_refund + store = @tpv_orbital_gateway.store(@mastercard_card_tpv, @options) + assert_success store + auth = @tpv_orbital_gateway.authorize(38, store.authorization, @options.merge(card_brand: 'MC')) + assert_success auth + response = @tpv_orbital_gateway.refund(38, auth.authorization, @options.merge(card_brand: 'MC')) + assert_success response + end + + def test_failed_refund_wrong_token + store = @tpv_orbital_gateway.store(@mastercard_card_tpv, @options) + assert_success store + auth = @tpv_orbital_gateway.authorize(38, store.authorization, @options.merge(card_brand: 'MC')) + assert_success auth + authorization = store.authorization.split(';').values_at(2).first + response = @tpv_orbital_gateway.refund(38, authorization, @options.merge(card_brand: 'MC')) + assert_failure response + assert_equal response.params['status_msg'], "The LIDM you supplied (#{authorization}) does not match with any existing transaction" + end + + def test_successful_purchase_with_stored_token_and_refund + store = @tpv_orbital_gateway.store(@mastercard_card_tpv, @options) + assert_success store + purchase = @tpv_orbital_gateway.purchase(38, store.authorization, @options.merge(card_brand: 'MC')) + assert_success purchase + response = @tpv_orbital_gateway.refund(38, purchase.authorization, @options.merge(card_brand: 'MC')) + assert_success response + end + + def test_successful_purchase_without_store + response = @tpv_orbital_gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal response.params['safetech_token'], nil + end + + def test_failed_purchase_with_stored_token + auth = @tpv_orbital_gateway.authorize(@amount, @credit_card, @options.merge(store: true)) + assert_success auth + options = @options.merge!(card_brand: 'VI') + response = @tpv_orbital_gateway.purchase(@amount, nil, options) + assert_failure response + assert_equal response.params['status_msg'], 'Error validating card/account number range' + end + def test_successful_different_cards @credit_card.brand = 'master' response = @gateway.verify(@credit_card, @options) @@ -1133,7 +1244,13 @@ def setup tax_indicator: '1', tax: '75', purchase_order: '123abc', - zip: address[:zip] + zip: address[:zip], + requestor_name: 'ArtVandelay123', + total_tax_amount: '75', + pst_tax_reg_number: '8675309', + customer_vat_reg_number: '1234567890', + commodity_code: 'SUMM', + local_tax_rate: '6250' } @level_3_options = { @@ -1143,7 +1260,11 @@ def setup dest_country: 'USA', discount_amount: 1, vat_tax: 1, - vat_rate: 25 + vat_rate: 25, + invoice_discount_treatment: 1, + tax_treatment: 1, + ship_vat_rate: 10, + unique_vat_invoice_ref: 'ABC123' } @line_items = [ @@ -1202,6 +1323,13 @@ def test_successful_purchase_with_level_2_data assert_equal 'Approved', response.message end + def test_successful_purchase_with_level_2_data_canadian_currency + response = @tandem_gateway.purchase(@amount, @credit_card, @options.merge(currency: 'CAD', merchant_vat_reg_number: '987654321', national_tax: '625', level_2_data: @level_2_options)) + + assert_success response + assert_equal 'Approved', response.message + end + def test_successful_purchase_with_level_3_data response = @tandem_gateway.purchase(@amount, @credit_card, @options.merge(level_2_data: @level_2_options, level_3_data: @level_3_options, line_items: @line_items)) @@ -1226,11 +1354,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 @@ -1238,11 +1368,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 @@ -1250,11 +1382,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 b10e5119e3a..611fec465a3 100644 --- a/test/remote/gateways/remote_pay_trace_test.rb +++ b/test/remote/gateways/remote_pay_trace_test.rb @@ -17,11 +17,11 @@ def setup @gateway = PayTraceGateway.new(fixtures(:pay_trace)) @amount = 100 - @credit_card = credit_card('4012000098765439') - @mastercard = credit_card('5499740000000057') + @credit_card = credit_card('4012000098765439', verification_value: '999') + @mastercard = credit_card('5499740000000057', verification_value: '998') @invalid_card = credit_card('54545454545454', month: '14', year: '1999') - @discover = credit_card('6011000993026909') - @amex = credit_card('371449635392376') + @discover = credit_card('6011000993026909', verification_value: '996') + @amex = credit_card('371449635392376', verification_value: '9997') @echeck = check(account_number: '123456', routing_number: '325070760') @options = { billing_address: { @@ -39,6 +39,15 @@ def test_acquire_token assert_not_nil response['access_token'] end + def test_failed_access_token + error = assert_raises(ActiveMerchant::OAuthResponseError) do + gateway = PayTraceGateway.new(username: 'username', password: 'password', integrator_id: 'uniqueintegrator') + gateway.send :acquire_access_token + end + + assert_equal error.message, 'Failed with The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' + end + def test_successful_purchase response = @gateway.purchase(1000, @credit_card, @options) assert_success response @@ -390,13 +399,6 @@ def test_duplicate_customer_creation # gateway is with a specific dollar amount. Since verify is auth and void combined, # having separate tests for auth and void should suffice. - def test_invalid_login - gateway = PayTraceGateway.new(username: 'username', password: 'password', integrator_id: 'integrator_id') - - response = gateway.acquire_access_token - assert_match 'invalid_grant', response - end - def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @amex, @options) diff --git a/test/remote/gateways/remote_payeezy_test.rb b/test/remote/gateways/remote_payeezy_test.rb index ebe148841a8..da2fd0fae93 100644 --- a/test/remote/gateways/remote_payeezy_test.rb +++ b/test/remote/gateways/remote_payeezy_test.rb @@ -50,6 +50,16 @@ def setup 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 @@ -86,6 +96,19 @@ def test_successful_purchase_with_apple_pay 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) @@ -99,6 +122,26 @@ def test_successful_purchase_with_soft_descriptors assert_success response end + def test_successful_purchase_and_authorize_with_reference_3 + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(reference_3: '123345')) + assert_match(/Transaction Normal/, response.message) + assert_success response + + assert auth = @gateway.authorize(@amount, @credit_card, @options.merge(reference_3: '123345')) + assert_match(/Transaction Normal/, auth.message) + assert_success auth + end + + def test_successful_purchase_and_authorize_with_customer_ref_top_level + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(customer_ref: 'abcde')) + assert_match(/Transaction Normal/, response.message) + assert_success response + + assert auth = @gateway.authorize(@amount, @credit_card, @options.merge(customer_ref: 'abcde')) + assert_match(/Transaction Normal/, auth.message) + assert_success auth + end + def test_successful_purchase_with_customer_ref assert response = @gateway.purchase(@amount, @credit_card, @options.merge(level2: { customer_ref: 'An important customer' })) assert_match(/Transaction Normal/, response.message) @@ -148,6 +191,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 @@ -376,7 +461,7 @@ def test_trans_error assert response = @gateway.purchase(@amount, @credit_card, @options) assert_match(/Server Error/, response.message) # 42 is 'unable to send trans' assert_failure response - assert_equal '500', response.error_code + assert_equal '500 INTERNAL_SERVER_ERROR', response.error_code end def test_transcript_scrubbing_store 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..e2a504648be 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, + year: Time.now.year + 1, 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, @@ -30,7 +32,7 @@ def setup @eci = '01' @three_ds_v1_version = '1.0.2' @three_ds_v2_version = '2.1.0' - @three_ds_server_trans_id = 'three-ds-v2-trans-id' + @ds_server_trans_id = 'ffffffff-9002-51a3-8000-0000000345a2' @authentication_response_status = 'Y' @three_ds_v1_mpi = { @@ -44,7 +46,7 @@ def setup cavv: @cavv, eci: @eci, version: @three_ds_v2_version, - three_ds_server_trans_id: @three_ds_server_trans_id, + ds_transaction_id: @ds_server_trans_id, authentication_response_status: @authentication_response_status } end @@ -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 c7943d6801a..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 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_pin_test.rb b/test/remote/gateways/remote_pin_test.rb index 003b2729601..eaae9661ffa 100644 --- a/test/remote/gateways/remote_pin_test.rb +++ b/test/remote/gateways/remote_pin_test.rb @@ -47,6 +47,20 @@ def test_successful_purchase_with_metadata assert_equal options_with_metadata[:metadata][:purchase_number], response.params['response']['metadata']['purchase_number'] end + def test_successful_purchase_with_platform_adjustment + options_with_platform_adjustment = { + platform_adjustment: { + amount: 30, + currency: 'AUD' + } + } + response = @gateway.purchase(@amount, @credit_card, @options.merge(options_with_platform_adjustment)) + assert_success response + assert_equal true, response.params['response']['captured'] + assert_equal options_with_platform_adjustment[:platform_adjustment][:amount], response.params['response']['platform_adjustment']['amount'] + assert_equal options_with_platform_adjustment[:platform_adjustment][:currency], response.params['response']['platform_adjustment']['currency'] + end + def test_successful_purchase_with_reference response = @gateway.purchase(@amount, @credit_card, @options.merge(reference: 'statement descriptor')) assert_success response diff --git a/test/remote/gateways/remote_plexo_test.rb b/test/remote/gateways/remote_plexo_test.rb index 433a8a72245..88f70b20de6 100644 --- a/test/remote/gateways/remote_plexo_test.rb +++ b/test/remote/gateways/remote_plexo_test.rb @@ -31,6 +31,22 @@ def setup description: 'Test desc', reason: 'requested by client' } + + @network_token_credit_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + first_name: 'Santiago', last_name: 'Navatta', + brand: 'Mastercard', + payment_cryptogram: 'UnVBR0RlYm42S2UzYWJKeWJBdWQ=', + number: '5555555555554444', + source: :network_token, + month: '12', + year: Time.now.year + }) + end + + def test_successful_purchase_with_network_token + response = @gateway.purchase(@amount, @network_token_credit_card, @options.merge({ invoice_number: '12345abcde' })) + assert_success response + assert_equal 'You have been mocked.', response.message end def test_successful_purchase @@ -43,6 +59,24 @@ def test_successful_purchase_with_finger_print assert_success response end + def test_successful_purchase_with_invoice_number + response = @gateway.purchase(@amount, @credit_card, @options.merge({ invoice_number: '12345abcde' })) + assert_success response + assert_equal '12345abcde', response.params['invoiceNumber'] + end + + def test_successfully_send_merchant_id + # ensures that we can set and send the merchant_id and get a successful response + response = @gateway.purchase(@amount, @credit_card, @options.merge({ merchant_id: 3243 })) + assert_success response + assert_equal 3243, response.params['merchant']['id'] + + # ensures that we can set and send the merchant_id and expect a failed response for invalid merchant_id + response = @gateway.purchase(@amount, @credit_card, @options.merge({ merchant_id: 1234 })) + assert_failure response + assert_equal 'The requested Merchant was not found.', response.message + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -85,9 +119,12 @@ def test_partial_capture end def test_failed_capture - response = @gateway.capture(@amount, '123') + auth = @gateway.authorize(@amount, @declined_card, @options) + assert_failure auth + + response = @gateway.capture(@amount, auth.authorization) assert_failure response - assert_equal 'An internal error occurred. Contact support.', response.message + assert_equal 'The selected payment state is not valid.', response.message end def test_successful_refund @@ -107,9 +144,12 @@ def test_partial_refund end def test_failed_refund - response = @gateway.refund(@amount, '123', @cancel_options) + auth = @gateway.authorize(@amount, @declined_card, @options) + assert_failure auth + + response = @gateway.refund(@amount, auth.authorization, @cancel_options) assert_failure response - assert_equal 'An internal error occurred. Contact support.', response.message + assert_equal 'The selected payment state is not valid.', response.message end def test_successful_void @@ -121,9 +161,12 @@ def test_successful_void end def test_failed_void - response = @gateway.void('123', @cancel_options) + auth = @gateway.authorize(@amount, @declined_card, @options) + assert_failure auth + + response = @gateway.void(auth.authorization, @cancel_options) assert_failure response - assert_equal 'An internal error occurred. Contact support.', response.message + assert_equal 'The selected payment state is not valid.', response.message end def test_successful_verify @@ -136,6 +179,11 @@ def test_successful_verify_with_custom_amount assert_success response end + def test_successful_verify_with_invoice_number + response = @gateway.verify(@credit_card, @options.merge({ invoice_number: '12345abcde' })) + assert_success response + end + def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response @@ -261,6 +309,6 @@ def test_successful_purchase_and_declined_cancellation_sodexo assert_success purchase assert void = @gateway.void(purchase.authorization, @cancel_options) - assert_success void + assert_failure void end end diff --git a/test/remote/gateways/remote_priority_test.rb b/test/remote/gateways/remote_priority_test.rb index ef3849fb283..d70fe153b42 100644 --- a/test/remote/gateways/remote_priority_test.rb +++ b/test/remote/gateways/remote_priority_test.rb @@ -70,6 +70,51 @@ def setup } ] } + + @all_gateway_fields = { + is_auth: true, + invoice: '123', + source: 'test', + replay_id: @replay_id, + ship_amount: 1, + ship_to_country: 'US', + ship_to_zip: '12345', + payment_type: '', + tender_type: '', + tax_exempt: true, + pos_data: { + cardholder_presence: 'NotPresent', + device_attendance: 'Unknown', + device_input_capability: 'KeyedOnly', + device_location: 'Unknown', + pan_capture_method: 'Manual', + partial_approval_support: 'Supported', + pin_capture_capability: 'Twelve' + }, + purchases: [ + { + line_item_id: 79402, + name: 'Book', + description: 'The Elements of Style', + quantity: 1, + unit_price: 1.23, + discount_amount: 0, + extended_amount: '1.23', + discount_rate: 0, + tax_amount: 1 + }, + { + line_item_id: 79403, + name: 'Cat Poster', + description: 'A sleeping cat', + quantity: 1, + unit_price: '2.34', + discount_amount: 0, + extended_amount: '2.34', + discount_rate: 0 + } + ] + } end def test_successful_authorize @@ -162,6 +207,15 @@ def test_successful_purchase_with_additional_options assert_equal 'Approved or completed successfully', response.message end + def test_successful_authorize_and_capture_with_auth_purchase_params + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + capture = @gateway.capture(@amount, auth.authorization, @all_gateway_fields) + assert_success capture + assert_equal 'Approved', capture.message + end + def test_successful_credit options = @options.merge(@additional_creditoptions) response = @gateway.credit(@credit_amount, @credit_card, options) @@ -267,7 +321,7 @@ def test_failed_get_payment_status def test_successful_verify response = @gateway.verify(credit_card('411111111111111')) assert_success response - assert_match 'JPMORGAN CHASE BANK, N.A.', response.params['bank']['name'] + assert_match 'JPMORGAN CHASE BANK N.A.', response.params['bank']['name'] end def test_failed_verify 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_quickbooks_test.rb b/test/remote/gateways/remote_quickbooks_test.rb index f3457706af5..a319954a7ce 100644 --- a/test/remote/gateways/remote_quickbooks_test.rb +++ b/test/remote/gateways/remote_quickbooks_test.rb @@ -16,6 +16,12 @@ def setup state: 'CA' }), description: 'Store Purchase' } + + @amex = credit_card( + '378282246310005', + verification_value: '1234', + brand: 'american_express' + ) end def test_successful_purchase @@ -128,6 +134,18 @@ def test_transcript_scrubbing assert_scrubbed(@gateway.options[:refresh_token], transcript) end + def test_transcript_scrubbing_for_amex + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @amex, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@amex.number, transcript) + assert_scrubbed(@amex.verification_value, transcript) + assert_scrubbed(@gateway.options[:access_token], transcript) + assert_scrubbed(@gateway.options[:refresh_token], transcript) + end + def test_failed_purchase_with_expired_token @gateway.options[:access_token] = 'not_a_valid_token' response = @gateway.purchase(@amount, @credit_card, @options) diff --git a/test/remote/gateways/remote_rapyd_test.rb b/test/remote/gateways/remote_rapyd_test.rb index 6aead2a6dc4..40e9564cc17 100644 --- a/test/remote/gateways/remote_rapyd_test.rb +++ b/test/remote/gateways/remote_rapyd_test.rb @@ -3,7 +3,7 @@ class RemoteRapydTest < Test::Unit::TestCase def setup @gateway = RapydGateway.new(fixtures(:rapyd)) - + @gateway_payment_redirect = RapydGateway.new(fixtures(:rapyd).merge(url_override: 'payment_redirect')) @amount = 100 @credit_card = credit_card('4111111111111111', first_name: 'Ryan', last_name: 'Reynolds', month: '12', year: '2035', verification_value: '345') @declined_card = credit_card('4111111111111105') @@ -16,29 +16,43 @@ def setup description: 'Describe this transaction', statement_descriptor: 'Statement Descriptor', email: 'test@example.com', - billing_address: address(name: 'Jim Reynolds') + 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': [ - { 'name': 'John Doe' }, - { 'type': 'customer' } + array_of_objects: [ + { name: 'John Doe' }, + { type: 'customer' } ], - 'array_of_strings': %w[ + array_of_strings: %w[ color size ], - 'number': 1234567890, - 'object': { - 'string': 'person' + number: 1234567890, + object: { + string: 'person' }, - 'string': 'preferred', - 'Boolean': true + string: 'preferred', + Boolean: true } @three_d_secure = { version: '2.1.0', @@ -46,6 +60,8 @@ def setup 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 @@ -54,13 +70,77 @@ def test_successful_purchase assert_equal 'SUCCESS', response.message end + def test_successful_purchase_for_idempotent_requests + response = @gateway.purchase(@amount, @credit_card, @options.merge(idempotency_key: '1234567890')) + assert_success response + assert_equal 'SUCCESS', response.message + original_operation_id = response.params['status']['operation_id'] + original_data_id = response.params['data']['id'] + idempotent_request = @gateway.purchase(@amount, @credit_card, @options.merge(idempotency_key: '1234567890')) + assert_success idempotent_request + assert_equal 'SUCCESS', idempotent_request.message + assert_equal original_operation_id, idempotent_request.params['status']['operation_id'] + assert_equal original_data_id, idempotent_request.params['data']['id'] + end + + def test_successful_purchase_for_non_idempotent_requests + # is not a idemptent request due the amount is different + response = @gateway.purchase(@amount, @credit_card, @options.merge(idempotency_key: '1234567890')) + assert_success response + assert_equal 'SUCCESS', response.message + original_operation_id = response.params['status']['operation_id'] + idempotent_request = @gateway.purchase(25, @credit_card, @options.merge(idempotency_key: '1234567890')) + assert_success idempotent_request + assert_equal 'SUCCESS', idempotent_request.message + assert_not_equal original_operation_id, idempotent_request.params['status']['operation_id'] + 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 - @options[:currency] = 'EUR' - @options[:pm_type] = 'gi_visa_card' - @options[:complete_payment_url] = 'https://www.rapyd.net/platform/collect/online/' - @options[:error_payment_url] = 'https://www.rapyd.net/platform/collect/online/' + # 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 - response = @gateway.purchase(15000, @credit_card, @options.merge({ stored_credential: { network_transaction_id: '123456', reason_type: 'recurring' } })) + def test_successful_purchase_with_reccurence_type + @options[:pm_type] = 'gb_visa_mo_card' + response = @gateway.purchase(@amount, @credit_card, @options.merge(recurrence_type: 'recurring')) assert_success response assert_equal 'SUCCESS', response.message end @@ -73,6 +153,18 @@ def test_successful_purchase_with_address assert_equal 'SUCCESS', response.message end + 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 + def test_successful_purchase_using_ach response = @gateway.purchase(100000, @check, @ach_options) assert_success response @@ -81,7 +173,7 @@ def test_successful_purchase_using_ach 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 @@ -163,37 +255,39 @@ def test_failed_void_with_payment_method_error assert void = @gateway.void(auth.authorization) assert_failure void - assert_equal 'ERROR_PAYMENT_METHOD_TYPE_DOES_NOT_SUPPORT_PAYMENT_CANCELLATION', void.error_code + assert_equal 'ERROR_PAYMENT_METHOD_TYPE_DOES_NOT_SUPPORT_PAYMENT_CANCELLATION', void.params['status']['response_code'] + end + + def test_failed_authorize_with_payment_method_type_error + auth = @gateway_payment_redirect.authorize(@amount, @credit_card, @options.merge(pm_type: 'worng_type')) + assert_failure auth + assert_equal 'ERROR', auth.params['status']['status'] + assert_equal 'ERROR_GET_PAYMENT_METHOD_TYPE', auth.params['status']['response_code'] + end + + def test_failed_purchase_with_zero_amount + response = @gateway_payment_redirect.purchase(0, @credit_card, @options) + assert_failure response + assert_equal 'ERROR', response.params['status']['status'] + assert_equal 'ERROR_CARD_VALIDATION_CAPTURE_TRUE', response.params['status']['response_code'] end def test_failed_void response = @gateway.void('') assert_failure response - assert_equal 'UNAUTHORIZED_API_CALL', response.message + assert_equal 'NOT_FOUND', response.message end def test_successful_verify - response = @gateway.verify(@credit_card, @options.except(:billing_address)) + response = @gateway.verify(@credit_card, @options) assert_success response assert_equal 'SUCCESS', response.message 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 @@ -211,8 +305,9 @@ def test_successful_store_and_purchase assert store.params.dig('data', 'default_payment_method') # 3DS authorization is required on storing a payment method for future transactions - # purchase = @gateway.purchase(100, store.authorization, @options.merge(customer_id: customer_id)) - # assert_sucess purchase + # 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 @@ -239,6 +334,7 @@ def test_failed_unstore unstore = @gateway.unstore('') assert_failure unstore + assert_equal 'NOT_FOUND', unstore.message end def test_invalid_login @@ -256,7 +352,7 @@ def test_transcript_scrubbing transcript = @gateway.scrub(transcript) assert_scrubbed(@credit_card.number, transcript) - assert_scrubbed(@credit_card.verification_value, transcript) + assert_scrubbed(/"#{@credit_card.verification_value}"/, transcript) assert_scrubbed(@gateway.options[:secret_key], transcript) assert_scrubbed(@gateway.options[:access_key], transcript) end @@ -295,4 +391,127 @@ def test_successful_authorize_with_3ds_v2_options 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 + + def test_successful_purchase_payment_redirect_url + response = @gateway_payment_redirect.purchase(@amount, @credit_card, @options.merge(pm_type: 'gb_visa_mo_card')) + + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_3ds_v2_gateway_specific_payment_redirect_url + options = @options.merge(three_d_secure: { required: true }) + options[:pm_type] = 'gb_visa_card' + + response = @gateway_payment_redirect.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'] + end + + def test_successful_purchase_without_cvv_payment_redirect_url + options = @options.merge({ pm_type: 'gb_visa_card', network_transaction_id: rand.to_s[2..11] }) + @credit_card.verification_value = nil + response = @gateway_payment_redirect.purchase(100, @credit_card, options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_refund_payment_redirect_url + purchase = @gateway_payment_redirect.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal 'SUCCESS', refund.message + end + + def test_successful_subsequent_purchase_stored_credential_payment_redirect_url + response = @gateway_payment_redirect.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_fx_fields_with_currency_exchange + @options[:pm_type] = 'gb_visa_card' + @options[:currency] = 'GBP' + @options[:requested_currency] = 'USD' + @options[:fixed_side] = 'buy' + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_fx_fields_us_debit_card + @options[:currency] = 'EUR' + @options[:requested_currency] = 'USD' + @options[:fixed_side] = 'buy' + + response = @gateway.purchase(@amount, @credit_card, @options) + 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 index b72af06f159..9aaf8b59fda 100644 --- a/test/remote/gateways/remote_reach_test.rb +++ b/test/remote/gateways/remote_reach_test.rb @@ -320,7 +320,6 @@ def test_transcript_scrubbing 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 + raw_response.match(/(gip_device_fingerprint=')([\w -]+)/)[2] 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_rest_test.rb b/test/remote/gateways/remote_redsys_rest_test.rb new file mode 100644 index 00000000000..6c4f2361e59 --- /dev/null +++ b/test/remote/gateways/remote_redsys_rest_test.rb @@ -0,0 +1,210 @@ +require 'test_helper' + +class RemoteRedsysRestTest < Test::Unit::TestCase + def setup + @gateway = RedsysRestGateway.new(fixtures(:redsys_rest)) + @amount = 100 + @credit_card = credit_card('4548812049400004') + @credit_card_no_cvv = credit_card('4548812049400004', verification_value: nil) + @declined_card = credit_card + @threeds2_credit_card = credit_card('4918019199883839') + + @threeds2_credit_card_frictionless = credit_card('4548814479727229') + @threeds2_credit_card_alt = credit_card('4548817212493017') + @options = { + order_id: generate_order_id + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Transaction Approved', response.message + end + + def test_purchase_with_invalid_order_id + response = @gateway.purchase(@amount, @credit_card, order_id: "a%4#{generate_order_id}") + assert_success response + assert_equal 'Transaction Approved', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_equal 'Refusal with no specific reason', response.message + end + + def test_purchase_and_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + refund = @gateway.refund(@amount, purchase.authorization, @options) + assert_success refund + end + + def test_purchase_and_failed_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + refund = @gateway.refund(@amount + 100, purchase.authorization, @options) + assert_failure refund + assert_equal 'SIS0057 ERROR', refund.message + end + + def test_failed_purchase_with_unsupported_currency + response = @gateway.purchase(600, @credit_card, @options.merge(currency: 'PEN')) + assert_failure response + assert_equal 'SIS0027 ERROR', response.message + end + + def test_successful_authorize_and_capture + authorize = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize + assert_equal 'Transaction Approved', authorize.message + assert_not_nil authorize.authorization + + capture = @gateway.capture(@amount, authorize.authorization, @options) + assert_success capture + assert_match(/Refund.*approved/, capture.message) + end + + def test_successful_authorize_and_failed_capture + authorize = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize + assert_equal 'Transaction Approved', authorize.message + assert_not_nil authorize.authorization + + capture = @gateway.capture(2 * @amount, authorize.authorization, @options) + assert_failure capture + assert_match(/SIS0062 ERROR/, capture.message) + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_equal 'Refusal with no specific reason', response.message + end + + def test_successful_void + authorize = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize + + void = @gateway.void(authorize.authorization, @options) + assert_success void + assert_equal '100', void.params['ds_amount'] + assert_equal 'Cancellation Accepted', void.message + end + + def test_failed_void + authorize = @gateway.authorize(@amount, @credit_card, @options) + assert_success authorize + + authorization = "#{authorize.params[:ds_order]}|#{@amount}|203" + void = @gateway.void(authorization, @options) + assert_failure void + assert_equal 'SIS0027 ERROR', void.message + end + + def test_successful_verify + assert response = @gateway.verify(@credit_card, @options) + assert_success response + + assert_equal 'Transaction Approved', response.message + end + + def test_successful_verify_without_cvv + assert response = @gateway.verify(@credit_card_no_cvv, @options) + assert_success response + + assert_equal 'Transaction Approved', response.message + end + + def test_unsuccessful_verify + assert response = @gateway.verify(@declined_card, @options) + assert_failure response + assert_equal 'Refusal with no specific reason', response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + clean_transcript = @gateway.scrub(transcript) + + assert_scrubbed(@gateway.options[:secret_key], clean_transcript) + assert_scrubbed(@credit_card.number, clean_transcript) + assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) + end + + def test_transcript_scrubbing_on_failed_transactions + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @declined_card, @options) + end + clean_transcript = @gateway.scrub(transcript) + + assert_scrubbed(@gateway.options[:secret_key], clean_transcript) + assert_scrubbed(@credit_card.number, clean_transcript) + assert_scrubbed(@credit_card.verification_value.to_s, clean_transcript) + end + + def test_encrypt_handles_url_safe_character_in_secret_key_without_error + gateway = RedsysRestGateway.new({ + login: '091952713', + secret_key: 'yG78qf-PkHyRzRiZGSTCJdO2TvjWgFa8' + }) + response = gateway.purchase(@amount, @credit_card, @options) + assert response + end + + def test_successful_authorize_3ds_setup + options = @options.merge(execute_threed: true, terminal: 12) + response = @gateway.authorize(@amount, @credit_card, options) + assert_success response + assert response.params['ds_emv3ds'] + assert_equal '2.2.0', response.params['ds_emv3ds']['protocolVersion'] + assert_equal 'CardConfiguration', response.message + assert response.authorization + end + + def test_successful_purchase_3ds + options = @options.merge(execute_threed: true) + response = @gateway.purchase(@amount, @threeds2_credit_card, options) + assert_success response + assert three_ds_data = response.params['ds_emv3ds'] + assert_equal '2.1.0', three_ds_data['protocolVersion'] + assert_equal 'https://sis-d.redsys.es/sis-simulador-web/threeDsMethod.jsp', three_ds_data['threeDSMethodURL'] + assert_equal 'CardConfiguration', response.message + assert response.authorization + end + + # Pending 3DS support + # Requires account configuration to allow setting moto flag + # def test_purchase_with_moto_flag + # response = @gateway.purchase(@amount, @credit_card, @options.merge(moto: true, metadata: { manual_entry: true })) + # assert_equal 'SIS0488 ERROR', response.message + # end + + # Pending 3DS support + # def test_successful_3ds_authorize_with_exemption + # options = @options.merge(execute_threed: true, terminal: 12) + # response = @gateway.authorize(@amount, @credit_card, options.merge(sca_exemption: 'LWV')) + # assert_success response + # assert response.params['ds_emv3ds'] + # assert_equal 'NO_3DS_v2', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] + # assert_equal 'CardConfiguration', response.message + # end + + # Pending 3DS support + # def test_successful_3ds_purchase_with_exemption + # options = @options.merge(execute_threed: true, terminal: 12) + # response = @gateway.purchase(@amount, @credit_card, options.merge(sca_exemption: 'LWV')) + # assert_success response + # assert response.params['ds_emv3ds'] + # assert_equal 'NO_3DS_v2', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] + # assert_equal 'CardConfiguration', response.message + # end + + private + + def generate_order_id + (Time.now.to_f * 100).to_i.to_s + end +end diff --git a/test/remote/gateways/remote_redsys_sha256_test.rb b/test/remote/gateways/remote_redsys_sha256_test.rb index 334e4cc21ca..bd36ae2bb72 100644 --- a/test/remote/gateways/remote_redsys_sha256_test.rb +++ b/test/remote/gateways/remote_redsys_sha256_test.rb @@ -105,7 +105,7 @@ def test_successful_authorize_3ds response = @gateway.authorize(@amount, @credit_card, options) assert_success response assert response.params['ds_emv3ds'] - assert_equal 'NO_3DS_v2', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] + assert_equal '2.2.0', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] assert_equal 'CardConfiguration', response.message assert response.authorization end @@ -132,7 +132,7 @@ def test_successful_3ds_authorize_with_exemption response = @gateway.authorize(@amount, @credit_card, options.merge(sca_exemption: 'LWV')) assert_success response assert response.params['ds_emv3ds'] - assert_equal 'NO_3DS_v2', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] + assert_equal '2.2.0', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] assert_equal 'CardConfiguration', response.message end @@ -141,7 +141,7 @@ def test_successful_3ds_purchase_with_exemption response = @gateway.purchase(@amount, @credit_card, options.merge(sca_exemption: 'LWV')) assert_success response assert response.params['ds_emv3ds'] - assert_equal 'NO_3DS_v2', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] + assert_equal '2.2.0', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] assert_equal 'CardConfiguration', response.message end @@ -364,12 +364,9 @@ def test_failed_void authorize = @gateway.authorize(@amount, @credit_card, @options) assert_success authorize - void = @gateway.void(authorize.authorization) - assert_success void - - another_void = @gateway.void(authorize.authorization) + another_void = @gateway.void(authorize.authorization << '123') assert_failure another_void - assert_equal 'SIS0222 ERROR', another_void.message + assert_equal 'SIS0007 ERROR', another_void.message end def test_successful_verify @@ -377,8 +374,6 @@ def test_successful_verify assert_success response assert_equal 'Transaction Approved', response.message - assert_success response.responses.last, 'The void should succeed' - assert_equal 'Cancellation Accepted', response.responses.last.message end def test_unsuccessful_verify diff --git a/test/remote/gateways/remote_redsys_test.rb b/test/remote/gateways/remote_redsys_test.rb index b8c790d30f9..eef1dc8a108 100644 --- a/test/remote/gateways/remote_redsys_test.rb +++ b/test/remote/gateways/remote_redsys_test.rb @@ -175,12 +175,9 @@ def test_failed_void authorize = @gateway.authorize(@amount, @credit_card, @options) assert_success authorize - void = @gateway.void(authorize.authorization) - assert_success void - - another_void = @gateway.void(authorize.authorization) - assert_failure another_void - assert_equal 'SIS0222 ERROR', another_void.message + void = @gateway.void(authorize.authorization << '123') + assert_failure void + assert_equal 'SIS0007 ERROR', void.message end def test_successful_verify @@ -188,8 +185,6 @@ def test_successful_verify assert_success response assert_equal 'Transaction Approved', response.message - assert_success response.responses.last, 'The void should succeed' - assert_equal 'Cancellation Accepted', response.responses.last.message end def test_unsuccessful_verify diff --git a/test/remote/gateways/remote_safe_charge_test.rb b/test/remote/gateways/remote_safe_charge_test.rb index 158e1464518..5c9d81fc6b6 100644 --- a/test/remote/gateways/remote_safe_charge_test.rb +++ b/test/remote/gateways/remote_safe_charge_test.rb @@ -63,6 +63,16 @@ def test_successful_purchase assert_equal 'Success', response.message end + def test_successful_purchase_with_token + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Success', response.message + + subsequent_response = @gateway.purchase(@amount, response.authorization, @options) + assert_success subsequent_response + assert_equal 'Success', subsequent_response.message + end + def test_successful_purchase_with_non_fractional_currency options = @options.merge(currency: 'CLP') response = @gateway.purchase(127999, @credit_card, options) @@ -256,6 +266,28 @@ 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_unreferenced_refund_with_credit + option = { + unreferenced_refund: true + } + + assert general_credit = @gateway.credit(@amount, @credit_card, option) + assert_success general_credit + assert_equal 'Success', general_credit.message + end + def test_successful_credit response = @gateway.credit(@amount, credit_card('4444436501403986'), @options) assert_success response @@ -282,6 +314,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 e8de154a2aa..6015b5b3645 100644 --- a/test/remote/gateways/remote_sage_pay_test.rb +++ b/test/remote/gateways/remote_sage_pay_test.rb @@ -44,7 +44,7 @@ def setup ) @mastercard = CreditCard.new( - number: '5404000000000001', + number: '5186150660000009', month: 12, year: next_year, verification_value: 419, @@ -53,6 +53,16 @@ def setup brand: 'master' ) + @frictionless = CreditCard.new( + number: '5186150660000009', + month: 12, + year: next_year, + verification_value: 419, + first_name: 'SUCCESSFUL', + last_name: '', + brand: 'master' + ) + @electron = CreditCard.new( number: '4917300000000008', month: 12, @@ -64,9 +74,10 @@ def setup ) @declined_card = CreditCard.new( - number: '4111111111111111', + number: '4000000000000001', month: 9, year: next_year, + verification_value: 123, first_name: 'Tekin', last_name: 'Suleyman', brand: 'visa' @@ -97,9 +108,111 @@ def setup phone: '0161 123 4567' } + @options_v4 = { + billing_address: { + name: 'Tekin Suleyman', + address1: 'Flat 10 Lapwing Court', + address2: 'West Didsbury', + city: 'Manchester', + county: 'Greater Manchester', + country: 'GB', + zip: 'M20 2PS' + }, + shipping_address: { + name: 'Tekin Suleyman', + address1: '120 Grosvenor St', + city: 'Manchester', + county: 'Greater Manchester', + country: 'GB', + zip: 'M1 7QW' + }, + order_id: generate_unique_id, + description: 'Store purchase', + ip: '86.150.65.37', + email: 'tekin@tekin.co.uk', + phone: '0161 123 4567', + protocol_version: '4.00', + three_ds_2: { + channel: 'browser', + browser_info: { + accept_header: 'unknown', + depth: 48, + java: true, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown', + browser_size: '05' + }, + notification_url: 'https://example.com/notification' + } + } + @amount = 100 end + # Protocol 4 + def test_successful_purchase_v4 + assert response = @gateway.purchase(@amount, @mastercard, @options_v4) + assert_success response + + assert response.test? + assert !response.authorization.blank? + end + + def test_three_ds_challenge_purchase_v4 + assert response = @gateway.purchase(@amount, @mastercard, @options_v4.merge(apply_3d_secure: 1)) + + assert_equal '3DAUTH', response.params['Status'] + assert response.params.include?('ACSURL') + assert response.params.include?('CReq') + end + + def test_frictionless_purchase_v4 + assert response = @gateway.purchase(@amount, @frictionless, @options_v4.merge(apply_3d_secure: 1)) + assert_success response + + assert_equal 'OK', response.params['3DSecureStatus'] + end + + def test_successful_purchase_v4_cit + cit_options = @options_v4.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'installment' + }, + recurring_frequency: '30', + recurring_expiry: "#{Time.now.year + 1}-04-21", + installment_data: 5, + order_id: generate_unique_id + }) + assert response = @gateway.purchase(@amount, @mastercard, cit_options) + assert_success response + assert response.test? + assert !response.authorization.blank? + + network_transaction_id = response.params['SchemeTraceID'] + cit_options = @options_v4.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'installment', + network_transaction_id: network_transaction_id + }, + recurring_frequency: '30', + recurring_expiry: "#{Time.now.year + 1}-04-21", + installment_data: 5, + order_id: generate_unique_id + }) + assert response = @gateway.purchase(@amount, @mastercard, cit_options) + assert_success response + assert response.test? + assert !response.authorization.blank? + end + + # Protocol 3 def test_successful_mastercard_purchase assert response = @gateway.purchase(@amount, @mastercard, @options) assert_success response @@ -108,6 +221,14 @@ def test_successful_mastercard_purchase assert !response.authorization.blank? end + def test_protocol_version_v4_purchase + assert response = @gateway.purchase(@amount, @mastercard, @options.merge(protocol_version: '4.00')) + assert_failure response + + assert_equal 'MALFORMED', response.params['Status'] + assert_equal '3227 : The ThreeDSNotificationURL field is required.', response.message + end + def test_unsuccessful_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -142,10 +263,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 @@ -168,11 +286,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 @@ -272,15 +386,16 @@ def test_successful_purchase_with_gift_aid_payment assert_success response end - def test_successful_transaction_registration_with_apply_3d_secure - @options[:apply_3d_secure] = 1 - response = @gateway.purchase(@amount, @visa, @options) - # We receive a different type of response for 3D Secure requiring to - # redirect the user to the ACSURL given inside the response - assert response.params.include?('ACSURL') - assert_equal 'OK', response.params['3DSecureStatus'] - assert_equal '3DAUTH', response.params['Status'] - end + # Test failing on master and feature branch + # def test_successful_transaction_registration_with_apply_3d_secure + # @options[:apply_3d_secure] = 1 + # response = @gateway.purchase(@amount, @visa, @options) + # We receive a different type of response for 3D Secure requiring to + # redirect the user to the ACSURL given inside the response + # assert response.params.include?('ACSURL') + # assert_equal 'OK', response.params['3DSecureStatus'] + # assert_equal '3DAUTH', response.params['Status'] + # end def test_successful_purchase_with_account_type @options[:account_type] = 'E' @@ -402,7 +517,7 @@ def test_successful_verify def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response - assert_match(/Card Range not supported/, response.message) + assert_match(/5011 : Your card number has failed our validity checks and appears to be incorrect. Please check and re-enter./, response.message) end def test_transcript_scrubbing 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..6e6e8a82494 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 @@ -99,12 +107,15 @@ def test_authorization_and_capture def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response + assert_match CHARGE_ID_REGEX, response.authorization + assert_equal response.authorization, response.params['error']['chargeId'] + assert_equal response.message, 'The card was declined.' end 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 +127,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 +141,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 +155,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 +185,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 +197,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 index 5c13379c92f..013b89982ab 100644 --- a/test/remote/gateways/remote_shift4_test.rb +++ b/test/remote/gateways/remote_shift4_test.rb @@ -18,7 +18,8 @@ def setup tax: '2', customer_reference: 'D019D09309F2', destination_postal_code: '94719', - product_descriptors: %w(Hamburger Fries Soda Cookie) + product_descriptors: %w(Hamburger Fries Soda Cookie), + order_id: '123456' } @customer_address = { address1: '65 Easy St', @@ -78,6 +79,12 @@ def test_successful_purchase_with_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, @@ -173,13 +180,13 @@ def test_transcript_scrubbing end def test_failed_purchase - response = @gateway.purchase(@amount, @declined_card, @options) + response = @gateway.purchase(1500000000, @credit_card, @options) assert_failure response - assert_include response.message, 'Card for Merchant Id 0008628968 not found' + assert_include response.message, 'Transaction declined' end def test_failure_on_referral_transactions - response = @gateway.purchase(67800, @credit_card, @options) + response = @gateway.purchase(99999899, @credit_card, @options) assert_failure response assert_include 'Transaction declined', response.message end @@ -190,10 +197,18 @@ def test_failed_authorize assert_include response.message, 'Card for Merchant Id 0008628968 not found' end + def test_failed_authorize_with_failure_amount + # this amount triggers failure according to Shift4 docs + response = @gateway.authorize(1500000000, @credit_card, @options) + assert_failure response + assert_equal response.message, 'Transaction declined' + end + def test_failed_authorize_with_error_message - response = @gateway.authorize(@amount, @unsupported_card, @options) + # this amount triggers failure according to Shift4 docs + response = @gateway.authorize(1500000000, @credit_card, @options) assert_failure response - assert_equal response.message, 'Format \'UTF8: An unexpected continuatio\' invalid or incompatible with argument' + assert_equal response.message, 'Transaction declined' end def test_failed_capture @@ -203,9 +218,21 @@ def test_failed_capture end def test_failed_refund - response = @gateway.refund(@amount, 'YC', @options) + response = @gateway.refund(1919, @credit_card, @options) + assert_failure response + assert_include response.message, 'Transaction declined' + 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(1919, @credit_card, @options) assert_failure response - assert_include response.message, 'record not posted' + assert_include response.message, 'Transaction declined' end def test_successful_refund @@ -236,6 +263,13 @@ def test_failed_void 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) 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..f30bf15fb5a --- /dev/null +++ b/test/remote/gateways/remote_shift4_v2_test.rb @@ -0,0 +1,217 @@ +require 'test_helper' +require_relative 'remote_securion_pay_test' + +class RemoteShift4V2Test < RemoteSecurionPayTest + def setup + super + @gateway = Shift4V2Gateway.new(fixtures(:shift4_v2)) + + @options[:ip] = '127.0.0.1' + @bank_account = check( + routing_number: '021000021', + account_number: '4242424242424242', + account_type: 'savings' + ) + 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 + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_match CHARGE_ID_REGEX, response.authorization + assert_equal response.authorization, response.params['error']['chargeId'] + assert_equal response.message, 'The card was declined.' + end + + def test_successful_store_and_unstore + store = @gateway.store(@credit_card, @options) + assert_success store + assert card_id = store.params['defaultCardId'] + assert customer_id = store.params['cards'][0]['customerId'] + unstore = @gateway.unstore(card_id, customer_id: customer_id) + assert_success unstore + assert_equal unstore.params['id'], card_id + end + + def test_failed_unstore + store = @gateway.store(@credit_card, @options) + assert_success store + assert customer_id = store.params['cards'][0]['customerId'] + unstore = @gateway.unstore(nil, customer_id: customer_id) + assert_failure unstore + assert_equal unstore.params['error']['type'], 'invalid_request' + end + + def test_successful_purchase_with_a_savings_bank_account + @options[:billing_address] = address(country: 'US') + response = @gateway.purchase(@amount, @bank_account, @options) + + assert_success response + assert_equal 'Transaction approved', response.message + end + + def test_successful_purchase_with_a_checking_bank_account + @options[:billing_address] = address(country: 'US') + @bank_account.account_type = 'checking' + + response = @gateway.purchase(@amount, @bank_account, @options) + + assert_success response + assert_equal 'Transaction approved', response.message + end + + def test_successful_bank_account_store + @options[:billing_address] = address(country: 'US') + @bank_account.account_type = 'checking' + + response = @gateway.store(@bank_account, @options) + + assert_success response + assert_match(/^pm_/, response.authorization) + end + + def test_successful_credit_card_store_with_existent_customer_id + @options[:customer_id] = 'cust_gHrIXDZqIq9Jp2t78A1Wp8CT' + response = @gateway.store(@credit_card, @options) + + assert_success response + assert_match(/^card_/, response.authorization) + assert_match(/^card_/, response.params['id']) + end + + def test_successful_credit_card_store_without_customer_id + response = @gateway.store(@credit_card, @options) + + assert_success response + assert_equal 'foo@example.com', response.params['email'] + assert_match(/^card_/, response.authorization) + assert_match(/^cust_/, response.params['id']) + end + + def test_successful_purchase_with_an_stored_credit_card + @options[:customer_id] = 'cust_gHrIXDZqIq9Jp2t78A1Wp8CT' + response = @gateway.store(@credit_card, @options) + assert_success response + + response = @gateway.purchase(@amount, response.authorization, @options) + + assert_success response + assert_equal 'Transaction approved', response.message + end + + def test_successful_purchase_with_an_stored_bank_account + @options[:billing_address] = address(country: 'US') + @bank_account.account_type = 'checking' + + response = @gateway.store(@bank_account, @options) + assert_success response + + response = @gateway.purchase(@amount, response.authorization, @options) + + assert_success response + assert_equal 'Transaction approved', response.message + end + + def test_store_raises_error_on_invalid_payment_method + assert_raises(ArgumentError) do + @gateway.store('abc123', @options) + end + end + + def test_successful_purchase_with_a_corporate_savings_bank_account + @options[:billing_address] = address(country: 'US') + @bank_account.account_type = 'checking' + @bank_account.account_holder_type = 'business' + + response = @gateway.purchase(@amount, @bank_account, @options) + + assert_success response + assert_equal 'Transaction approved', response.message + end + + def test_successful_full_refund_with_a_savings_bank_account + @options[:billing_address] = address(country: 'US') + purchase = @gateway.purchase(@amount, @bank_account, @options) + assert_success purchase + assert purchase.authorization + + refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + + 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 + + assert refund.authorization + end +end diff --git a/test/remote/gateways/remote_simetrik_test.rb b/test/remote/gateways/remote_simetrik_test.rb index b1e2eb24daf..90f66e2f844 100644 --- a/test/remote/gateways/remote_simetrik_test.rb +++ b/test/remote/gateways/remote_simetrik_test.rb @@ -78,6 +78,22 @@ def setup } end + def test_failed_access_token + assert_raises(ActiveMerchant::OAuthResponseError) do + gateway = SimetrikGateway.new({ client_id: 'YOUR_CLIENT_ID', client_secret: 'YOUR_API_KEY', audience: 'audience_url' }) + gateway.send :fetch_access_token + end + end + + def test_failed_authorize_with_failed_access_token + error = assert_raises(ActiveMerchant::OAuthResponseError) do + gateway = SimetrikGateway.new({ client_id: 'YOUR_CLIENT_ID', client_secret: 'YOUR_API_KEY', audience: 'audience_url' }) + gateway.authorize(@amount, @credit_card, @authorize_options_success) + end + + assert_equal error.message, 'Failed with 401 Unauthorized' + end + def test_success_authorize response = @gateway.authorize(@amount, @credit_card, @authorize_options_success) assert_success response 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 932ed7bc85b..fe2769e5115 100644 --- a/test/remote/gateways/remote_stripe_payment_intents_test.rb +++ b/test/remote/gateways/remote_stripe_payment_intents_test.rb @@ -3,42 +3,68 @@ 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' @declined_payment_method = 'pm_card_chargeDeclined' @three_ds_moto_enabled = 'pm_card_authenticationRequiredOnSetup' @three_ds_authentication_required = 'pm_card_authenticationRequired' + @cvc_check_fails_credit_card = 'pm_card_cvcCheckFail' + @avs_fail_card = 'pm_card_avsFail' @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', verification_value: '737', month: 10, - year: 2028) - @three_ds_credit_card = credit_card('4000000000003220', + year: 2028 + ) + + @three_ds_not_required_card = credit_card( + '4000000000003055', verification_value: '737', month: 10, - year: 2028) - @three_ds_not_required_card = credit_card('4000000000003055', + year: 2028 + ) + + @three_ds_external_data_card = credit_card( + '4000002760003184', verification_value: '737', month: 10, - year: 2028) - @three_ds_external_data_card = credit_card('4000002760003184', + year: 2031 + ) + + @visa_card = credit_card( + '4242424242424242', verification_value: '737', month: 10, - year: 2031) - @visa_card = credit_card('4242424242424242', + year: 2028 + ) + + @visa_card_brand_choice = credit_card( + '4000002500001001', verification_value: '737', month: 10, - year: 2028) + year: 2028 + ) @google_pay = network_tokenization_credit_card( '4242424242424242', - payment_cryptogram: 'dGVzdGNyeXB0b2dyYW1YWFhYWFhYWFhYWFg9PQ==', + payment_cryptogram: 'AgAAAAAABk4DWZ4C28yUQAAAAAA=', source: :google_pay, brand: 'visa', eci: '05', @@ -50,7 +76,7 @@ def setup @apple_pay = network_tokenization_credit_card( '4242424242424242', - payment_cryptogram: 'dGVzdGNyeXB0b2dyYW1YWFhYWFhYWFhYWFg9PQ==', + payment_cryptogram: 'AMwBRjPWDnAgAA7Rls7mAoABFA==', source: :apple_pay, brand: 'visa', eci: '05', @@ -60,6 +86,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 +120,24 @@ 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_card_brand + options = { + currency: 'USD', + customer: @customer, + card_brand: 'cartes_bancaires' + } + assert purchase = @gateway.purchase(@amount, @visa_card_brand_choice, options) assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + assert purchase.params.dig('charges', 'data')[0]['balance_transaction'] + assert_equal purchase.params['payment_method_options']['card']['network'], 'cartes_bancaires' end def test_successful_purchase_with_shipping_address @@ -100,12 +152,15 @@ 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 @@ -143,72 +198,111 @@ def test_successful_purchase_with_level3_data def test_unsuccessful_purchase_google_pay_with_invalid_card_number options = { - currency: 'GBP' + currency: 'GBP', + new_ap_gp_route: true } @google_pay.number = '378282246310000' purchase = @gateway.purchase(@amount, @google_pay, options) - assert_equal 'The tokenization process fails. Your card number is incorrect.', purchase.message + assert_equal 'Your card number is incorrect.', purchase.message assert_false purchase.success? end def test_unsuccessful_purchase_google_pay_without_cryptogram options = { - currency: 'GBP' + currency: 'GBP', + new_ap_gp_route: true } @google_pay.payment_cryptogram = '' purchase = @gateway.purchase(@amount, @google_pay, options) - assert_equal "The tokenization process fails. Cards using 'tokenization_method=android_pay' require the 'cryptogram' field to be set.", purchase.message + assert_equal 'Missing required param: payment_method_options[card][network_token][cryptogram].', purchase.message assert_false purchase.success? end def test_unsuccessful_purchase_google_pay_without_month options = { - currency: 'GBP' + currency: 'GBP', + new_ap_gp_route: true } @google_pay.month = '' purchase = @gateway.purchase(@amount, @google_pay, options) - assert_equal 'The tokenization process fails. Missing required param: card[exp_month].', purchase.message + assert_equal 'Missing required param: payment_method_data[card][exp_month].', purchase.message assert_false purchase.success? end def test_successful_authorize_with_google_pay options = { - currency: 'GBP' + currency: 'GBP', + new_ap_gp_route: true } + @google_pay.eci = '5' + assert_match('5', @google_pay.eci) auth = @gateway.authorize(@amount, @google_pay, options) - - assert_match('android_pay', auth.responses.first.params.dig('token', 'card', 'tokenization_method')) assert auth.success? - assert_match('google_pay', auth.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type']) + assert_match('google_pay', auth.params.dig('charges', 'data')[0].dig('payment_method_details', 'card', 'wallet', 'type')) end - def test_successful_purchase_with_apple_pay + def test_successful_purchase_with_google_pay options = { - currency: 'GBP' + currency: 'GBP', + new_ap_gp_route: true } - purchase = @gateway.purchase(@amount, @apple_pay, options) - assert_match('apple_pay', purchase.responses.first.params.dig('token', 'card', 'tokenization_method')) + purchase = @gateway.purchase(@amount, @google_pay, options) assert purchase.success? - assert_match('apple_pay', purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type']) + assert_match('google_pay', purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type']) end - def test_succesful_purchase_with_connect_for_apple_pay + def test_successful_purchase_with_tokenized_visa options = { - stripe_account: @destination_account + currency: 'USD', + last_4: '4242' } - assert response = @gateway.purchase(@amount, @apple_pay, options) - assert_success response + + 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_succesful_application_with_connect_for_google_pay + def test_successful_purchase_with_google_pay_when_sending_the_billing_address options = { - stripe_account: @destination_account + currency: 'GBP', + billing_address: address, + new_ap_gp_route: true } - assert response = @gateway.purchase(@amount, @google_pay, options) - assert_success response + + purchase = @gateway.purchase(@amount, @google_pay, options) + assert purchase.success? + billing_address_line1 = purchase.params.dig('charges', 'data')[0]['billing_details']['address']['line1'] + assert_equal '456 My Street', billing_address_line1 + 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', + new_ap_gp_route: true + } + + purchase = @gateway.purchase(@amount, @apple_pay, options) + assert purchase.success? + 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, + new_ap_gp_route: true + } + + purchase = @gateway.purchase(@amount, @apple_pay, options) + assert purchase.success? + billing_address_line1 = purchase.params.dig('charges', 'data')[0]['billing_details']['address']['line1'] + assert_equal '456 My Street', billing_address_line1 + assert_match('apple_pay', purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type']) end def test_purchases_with_same_idempotency_key @@ -549,6 +643,33 @@ def test_create_setup_intent_with_setup_future_usage end end + def test_create_setup_intent_with_setup_future_usage_and_card_brand + response = @gateway.create_setup_intent(@visa_card_brand_choice, { + address: { + email: 'test@example.com', + name: 'John Doe', + line1: '1 Test Ln', + city: 'Durham', + tracking_number: '123456789' + }, + currency: 'USD', + card_brand: 'cartes_bancaires', + confirm: true, + execute_threed: true, + return_url: 'https://example.com' + }) + + assert_equal 'succeeded', response.params['status'] + assert_equal response.params['payment_method_options']['card']['network'], 'cartes_bancaires' + # 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 = response.params['id'] + assert si_response = @gateway.retrieve_setup_intent(setup_intent_id) + + assert_equal 'succeeded', si_response.params['status'] + assert_not_empty si_response.params.dig('latest_attempt', 'payment_method_details', 'card') + 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, { @@ -700,7 +821,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 :/ } }) @@ -730,6 +851,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, { @@ -785,6 +977,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, but a 3DS initiation flag was not included in the request.', response.message + assert_equal response.authorization, response.params['id'] end def test_create_payment_intent_with_shipping_address @@ -901,6 +1094,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', @@ -1167,6 +1378,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', @@ -1189,10 +1415,13 @@ def test_successful_store_with_true_validate_option def test_successful_verify options = { - customer: @customer + customer: @customer, + billing_address: address } - assert verify = @gateway.verify(@visa_payment_method, options) + assert verify = @gateway.verify(@visa_card, options) + assert_equal 'US', verify.params.dig('latest_attempt', 'payment_method_details', 'card', 'country') assert_equal 'succeeded', verify.params['status'] + assert_equal 'M', verify.cvv_result['code'] end def test_failed_verify @@ -1202,6 +1431,16 @@ def test_failed_verify assert verify = @gateway.verify(@declined_payment_method, options) assert_equal 'Your card was declined.', verify.message + + assert_not_nil verify.authorization + assert_equal verify.params.dig('error', 'setup_intent', 'id'), verify.authorization + 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 @@ -1295,4 +1534,32 @@ def test_transcript_scrubbing assert_scrubbed(@three_ds_credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:login], transcript) end + + def test_succeeded_cvc_check + options = {} + assert purchase = @gateway.purchase(@amount, @visa_card, options) + + assert_equal 'succeeded', purchase.params['status'] + assert_equal 'M', purchase.cvv_result.dig('code') + assert_equal 'CVV matches', purchase.cvv_result.dig('message') + end + + def test_failed_cvc_check + options = {} + assert purchase = @gateway.purchase(@amount, @cvc_check_fails_credit_card, options) + + assert_equal 'succeeded', purchase.params['status'] + assert_equal 'N', purchase.cvv_result.dig('code') + assert_equal 'CVV does not match', purchase.cvv_result.dig('message') + end + + def test_failed_avs_check + options = {} + assert purchase = @gateway.purchase(@amount, @avs_fail_card, options) + + assert_equal 'succeeded', purchase.params['status'] + assert_equal 'N', purchase.avs_result['code'] + assert_equal 'N', purchase.avs_result['postal_match'] + assert_equal 'N', purchase.avs_result['street_match'] + end end 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..b49448208f5 --- /dev/null +++ b/test/remote/gateways/remote_sum_up_test.rb @@ -0,0 +1,126 @@ +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_pay_to_email_credential_error + gateway = SumUpGateway.new(fixtures(:sum_up).merge(pay_to_email: 'example@example.com')) + response = gateway.purchase(@amount, @credit_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 'PAID', 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 'SUCCESSFUL', response.params['transactions'].first['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 'PAID', 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 + + # In Sum Up the account can only return checkout/purchase in pending or success status, + # to obtain a successful refund we will need an account that returns the checkout/purchase in successful status + # + # For the following refund tests configure in the fixtures => :sum_up_successful_purchase + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + transaction_id = purchase.params['transaction_id'] + assert_not_nil transaction_id + + response = @gateway.refund(@amount, transaction_id, {}) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_partial_refund + purchase = @gateway.purchase(@amount * 10, @credit_card, @options) + transaction_id = purchase.params['transaction_id'] + assert_not_nil transaction_id + + response = @gateway.refund(@amount, transaction_id, {}) + assert_success response + assert_equal 'Succeeded', response.message + end + + # In Sum Up to trigger the 3DS flow (next_step object) you need to an European account + # + # For this example configure in the fixtures => :sum_up_3ds + def test_trigger_3ds_flow + gateway = SumUpGateway.new(fixtures(:sum_up_3ds)) + options = @options.merge( + currency: 'EUR', + redirect_url: 'https://mysite.com/completed_purchase' + ) + purchase = gateway.purchase(@amount, @credit_card, options) + assert_success purchase + assert_equal 'Succeeded', purchase.message + assert_not_nil purchase.params['next_step'] + 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_vantiv_express_test.rb b/test/remote/gateways/remote_vantiv_express_test.rb new file mode 100644 index 00000000000..1659e796c66 --- /dev/null +++ b/test/remote/gateways/remote_vantiv_express_test.rb @@ -0,0 +1,375 @@ +require 'test_helper' + +class RemoteVantivExpressTest < Test::Unit::TestCase + def setup + @gateway = VantivExpressGateway.new(fixtures(:element)) + + @amount = rand(1000..2000) + @credit_card = credit_card('4000100011112224') + @declined_card = credit_card('6060704495764400') + @check = check + @options = { + billing_address: address, + description: 'Store Purchase' + } + + @google_pay_network_token = network_tokenization_credit_card( + '6011000400000000', + 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: '05', + transaction_id: 'abc123', + source: :apple_pay + ) + end + + def test_successful_purchase_and_refund + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Approved', response.message + + assert refund = @gateway.refund(@amount, response.authorization) + assert_success refund + assert_equal 'Approved', refund.message + end + + def test_failed_purchase + @amount = 20 + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_equal 'INVALID CARD INFO', response.message + end + + def test_successful_purchase_with_echeck + response = @gateway.purchase(@amount, @check, @options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_purchase_with_payment_account_token + response = @gateway.store(@credit_card, @options) + assert_success response + + response = @gateway.purchase(@amount, response.authorization, @options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_shipping_address + response = @gateway.purchase(@amount, @credit_card, @options.merge(shipping_address: address(address1: 'Shipping'))) + assert_success response + 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_string + response = @gateway.purchase(@amount, @credit_card, @options.merge(card_present_code: 'Present')) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_payment_type_string + response = @gateway.purchase(@amount, @credit_card, @options.merge(payment_type: 'NotUsed')) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_submission_type_string + response = @gateway.purchase(@amount, @credit_card, @options.merge(submission_type: 'NotUsed')) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_duplicate_check_disable_flag + amount = @amount + + response = @gateway.purchase(amount, @credit_card, @options.merge(duplicate_check_disable_flag: true)) + assert_success response + assert_equal 'Approved', response.message + + response = @gateway.purchase(amount, @credit_card, @options.merge(duplicate_check_disable_flag: false)) + assert_failure response + assert_equal 'Duplicate', response.message + + response = @gateway.purchase(amount, @credit_card, @options.merge(duplicate_check_disable_flag: 'true')) + assert_success response + assert_equal 'Approved', response.message + + response = @gateway.purchase(amount, @credit_card, @options.merge(duplicate_check_disable_flag: 'xxx')) + assert_failure response + assert_equal 'Duplicate', response.message + end + + def test_successful_purchase_with_duplicate_override_flag + amount = @amount + + response = @gateway.purchase(amount, @credit_card, @options.merge(duplicate_override_flag: true)) + assert_success response + assert_equal 'Approved', response.message + + response = @gateway.purchase(amount, @credit_card, @options.merge(duplicate_override_flag: false)) + assert_failure response + assert_equal 'Duplicate', response.message + + response = @gateway.purchase(amount, @credit_card, @options.merge(duplicate_override_flag: 'true')) + assert_success response + assert_equal 'Approved', response.message + + response = @gateway.purchase(amount, @credit_card, @options.merge(duplicate_override_flag: 'xxx')) + assert_failure response + assert_equal 'Duplicate', response.message + end + + def test_successful_purchase_with_terminal_id + response = @gateway.purchase(@amount, @credit_card, @options.merge(terminal_id: '02')) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_lodging_and_all_other_fields + lodging_options = { + order_id: '2', + billing_address: address.merge(zip: '87654'), + description: 'Store Purchase', + duplicate_override_flag: 'true', + lodging: { + agreement_number: SecureRandom.hex(12), + check_in_date: 20250910, + check_out_date: 20250915, + room_amount: 1000, + room_tax: 0, + no_show_indicator: 0, + duration: 5, + customer_name: 'francois dubois', + client_code: 'Default', + extra_charges_detail: '01', + extra_charges_amounts: 'Default', + prestigious_property_code: 'DollarLimit500', + special_program_code: 'AdvanceDeposit', + charge_type: 'Restaurant' + }, + card_holder_present_code: '2', + card_input_code: '4', + card_present_code: 'NotPresent', + cvv_presence_code: '2', + market_code: 'HotelLodging', + terminal_capability_code: 'ChipReader', + terminal_environment_code: 'LocalUnattended', + terminal_type: 'Mobile', + terminal_id: '0001', + ticket_number: 182726718192 + } + response = @gateway.purchase(@amount, @credit_card, lodging_options) + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_enum_fields + lodging_options = { + order_id: '2', + billing_address: address.merge(zip: '87654'), + description: 'Store Purchase', + duplicate_override_flag: 'true', + lodging: { + agreement_number: SecureRandom.hex(12), + check_in_date: 20250910, + check_out_date: 20250915, + room_amount: 1000, + room_tax: 0, + no_show_indicator: 0, + duration: 5, + customer_name: 'francois dubois', + client_code: 'Default', + extra_charges_detail: '01', + extra_charges_amounts: 'Default', + prestigious_property_code: 1, + special_program_code: 2, + charge_type: 2 + }, + card_holder_present_code: '2', + card_input_code: '4', + card_present_code: 0, + cvv_presence_code: 2, + market_code: 5, + terminal_capability_code: 5, + terminal_environment_code: 6, + terminal_type: 2, + terminal_id: '0001', + ticket_number: 182726718192 + } + response = @gateway.purchase(@amount, @credit_card, lodging_options) + assert_success response + 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_no_eci + @apple_pay_network_token.eci = nil + + response = @gateway.purchase(1202, @apple_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_capture_and_void_with_apple_pay + auth = @gateway.authorize(3100, @apple_pay_network_token, @options) + assert_success auth + + assert capture = @gateway.capture(3200, auth.authorization) + assert_success capture + assert_equal 'Approved', capture.message + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal 'Success', void.message + end + + def test_successful_verify_with_apple_pay + response = @gateway.verify(@apple_pay_network_token, @options) + assert_success response + assert_equal 'Success', response.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 + @amount = 20 + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_equal 'INVALID CARD INFO', response.message + end + + def test_failed_capture + response = @gateway.capture(@amount, '') + assert_failure response + assert_equal 'TransactionID required', response.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, '') + assert_failure response + assert_equal 'TransactionID required', response.message + end + + def test_successful_credit + credit_options = @options.merge({ ticket_number: '1', market_code: 'FoodRestaurant', merchant_supplied_transaction_id: '123' }) + credit = @gateway.credit(@amount, @credit_card, credit_options) + + assert_success credit + end + + def test_failed_credit + credit = @gateway.credit(nil, @credit_card, @options) + + assert_failure credit + assert_equal 'TransactionAmount required', credit.message + end + + def test_successful_partial_capture_and_void + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount - 1, auth.authorization) + assert_success capture + + assert void = @gateway.void(auth.authorization) + assert_success void + assert_equal 'Success', void.message + end + + def test_failed_void + response = @gateway.void('') + assert_failure response + assert_equal 'TransactionAmount required', response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal 'Success', response.message + end + + def test_successful_store + response = @gateway.store(@credit_card, @options) + assert_success response + assert_match %r{PaymentAccount created}, response.message + end + + def test_invalid_login + gateway = ElementGateway.new(account_id: '3', account_token: '3', application_id: '3', acceptor_id: '3', application_name: '3', application_version: '3') + + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match %r{Invalid AccountToken}, 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[:account_token], transcript) + end + + def test_transcript_scrubbing_with_echeck + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @check, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@check.account_number, transcript) + assert_scrubbed(@check.routing_number, transcript) + assert_scrubbed(@gateway.options[:account_token], transcript) + end +end diff --git a/test/remote/gateways/remote_vpos_test.rb b/test/remote/gateways/remote_vpos_test.rb index 4a75f8d1754..523053f7fea 100644 --- a/test/remote/gateways/remote_vpos_test.rb +++ b/test/remote/gateways/remote_vpos_test.rb @@ -109,7 +109,7 @@ def test_transcript_scrubbing 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) + assert_no_match(/token\\":\\"[^\[FILTERD\]]/, transcript) + assert_no_match(/card_encrypted_data\\":\\"[^\[FILTERD\]]/, transcript) end end diff --git a/test/remote/gateways/remote_vpos_without_key_test.rb b/test/remote/gateways/remote_vpos_without_key_test.rb index a17e98838f2..ca77727d60d 100644 --- a/test/remote/gateways/remote_vpos_without_key_test.rb +++ b/test/remote/gateways/remote_vpos_without_key_test.rb @@ -111,8 +111,8 @@ def test_transcript_scrubbing 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) + assert_no_match(/token\\":\\"[^\[FILTERD\]]/, transcript) + assert_no_match(/card_encrypted_data\\":\\"[^\[FILTERD\]]/, transcript) end def test_regenerate_encryption_key diff --git a/test/remote/gateways/remote_wompi_test.rb b/test/remote/gateways/remote_wompi_test.rb index 65c312a1153..dc7a8576a22 100644 --- a/test/remote/gateways/remote_wompi_test.rb +++ b/test/remote/gateways/remote_wompi_test.rb @@ -34,6 +34,11 @@ def test_successful_purchase_without_cvv assert_success response end + def test_successful_purchase_with_tip_in_cents + response = @gateway.purchase(@amount, @credit_card, @options.merge(tip_in_cents: 300)) + assert_success response + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response diff --git a/test/remote/gateways/remote_worldpay_test.rb b/test/remote/gateways/remote_worldpay_test.rb index dfa2fe11b68..07540d478e7 100644 --- a/test/remote/gateways/remote_worldpay_test.rb +++ b/test/remote/gateways/remote_worldpay_test.rb @@ -6,18 +6,23 @@ 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') @@ -26,14 +31,23 @@ def setup @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=' + ) + @visa_nt_credit_card_without_eci = network_tokenization_credit_card( + '4895370015293175', + source: :network_token, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) + @mastercard_nt_credit_card_without_eci = network_tokenization_credit_card( + '5555555555554444', source: :network_token, - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) @options = { order_id: generate_unique_id, @@ -45,11 +59,8 @@ def setup invoice_reference_number: 'INV12233565', customer_reference: 'CUST00000101', card_acceptor_tax_id: 'VAT1999292', - sales_tax: { - amount: '20', - exponent: '2', - currency: 'USD' - } + tax_amount: '20', + ship_from_postal_code: '43245' } } @@ -57,58 +68,32 @@ def setup 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: { + tax_amount: '20', + discount_amount: '1', + shipping_amount: '50', + duty_amount: '20', + line_items: [{ description: 'Laptop 14', product_code: 'LP00125', commodity_code: 'COM00125', quantity: '2', - unit_cost: { - amount: '1500', - exponent: '2', - currency: 'USD' - }, + unit_cost: '1500', 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' - } - } + discount_amount: '200', + tax_amount: '500', + total_amount: '3300' + }, + { + description: 'Laptop 15', + product_code: 'LP00125', + commodity_code: 'COM00125', + quantity: '2', + unit_cost: '1500', + unit_of_measure: 'each', + discount_amount: '200', + tax_amount: '500', + total_amount: '3300' + }] } } @@ -130,7 +115,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', @@ -139,15 +125,28 @@ 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', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: Time.new.year + 2, + source: :google_pay, + transaction_id: '123456789', + eci: '05' + ) - @google_pay_network_token = network_tokenization_credit_card('4444333322221111', + @google_pay_network_token_without_eci = 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 @@ -162,8 +161,22 @@ def test_successful_purchase_with_network_token assert_equal 'SUCCESS', response.message end - def test_successful_purchase_with_network_token_without_eci - assert response = @gateway.purchase(@amount, @nt_credit_card_without_eci, @options) + def test_successful_purchase_with_network_token_and_stored_credentials + stored_credential_params = stored_credential(:initial, :unscheduled, :merchant) + + assert response = @gateway.purchase(@amount, @nt_credit_card, @options.merge({ stored_credential: stored_credential_params })) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_network_token_without_eci_visa + assert response = @gateway.purchase(@amount, @visa_nt_credit_card_without_eci, @options) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_successful_purchase_with_network_token_without_eci_mastercard + assert response = @gateway.purchase(@amount, @mastercard_nt_credit_card_without_eci, @options) assert_success response assert_equal 'SUCCESS', response.message end @@ -184,6 +197,46 @@ def test_successful_authorize_with_card_holder_name_google_pay assert_equal 'SUCCESS', response.message end + def test_successful_authorize_without_eci_google_pay + response = @gateway.authorize(@amount, @google_pay_network_token_without_eci, @options) + assert_success response + assert_equal @amount, response.params['amount_value'].to_i + assert_equal 'GBP', response.params['amount_currency_code'] + assert_equal 'SUCCESS', response.message + end + + def test_successful_authorize_with_default_eci_google_pay + response = @gateway.authorize(@amount, @google_pay_network_token_without_eci, @options.merge({ use_default_eci: true })) + assert_success response + assert_equal @amount, response.params['amount_value'].to_i + assert_equal 'GBP', response.params['amount_currency_code'] + assert_equal 'SUCCESS', response.message + end + + def test_successful_authorize_with_google_pay_pan_only + response = @gateway.authorize(@amount, @credit_card, @options.merge!(wallet_type: :google_pay)) + assert_success response + assert_equal 'SUCCESS', response.message + end + + def test_purchase_with_google_pay_pan_only + assert auth = @gateway.purchase(@amount, @credit_card, @options.merge!(wallet_type: :google_pay)) + assert_success auth + assert_equal 'SUCCESS', auth.message + assert auth.authorization + end + + def test_successful_authorize_with_void_google_pay_pan_only + assert auth = @gateway.authorize(@amount, @credit_card, @options.merge!(wallet_type: :google_pay)) + assert_success auth + assert_equal 'authorize', auth.params['action'] + assert auth.authorization + assert capture = @gateway.capture(@amount, auth.authorization, @options.merge(authorization_validated: true)) + assert_success capture + assert void = @gateway.void(auth.authorization, @options.merge(authorization_validated: true)) + assert_success void + end + def test_successful_authorize_without_card_holder_name_apple_pay @apple_pay_network_token.first_name = '' @apple_pay_network_token.last_name = '' @@ -442,9 +495,8 @@ def test_successful_authorize_with_3ds ) assert first_message = @gateway.authorize(@amount, @threeDS_card, options) assert first_message.test? + assert first_message.success? refute first_message.authorization.blank? - refute first_message.params['issuer_url'].blank? - refute first_message.params['pa_request'].blank? refute first_message.params['cookie'].blank? refute first_message.params['session_id'].blank? end @@ -470,19 +522,13 @@ def test_successful_authorize_with_3ds2_challenge 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? + assert response.success? 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, - reason_type: 'unscheduled', - initiator: 'merchant', - network_transaction_id: nil - } + stored_credential_params = stored_credential(:initial, :unscheduled, :merchant) assert auth = @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) assert_success auth @@ -494,12 +540,31 @@ def test_successful_auth_and_capture_with_normalized_stored_credential assert_success capture @options[:order_id] = generate_unique_id - @options[:stored_credential] = { - initial_transaction: false, - reason_type: 'installment', - initiator: 'merchant', - network_transaction_id: auth.params['transaction_identifier'] - } + @options[:stored_credential] = stored_credential(:used, :installment, :merchant, network_transaction_id: auth.params['transaction_identifier']) + + assert next_auth = @gateway.authorize(@amount, @credit_card, @options) + assert next_auth.authorization + assert next_auth.params['scheme_response'] + assert next_auth.params['transaction_identifier'] + + assert capture = @gateway.capture(@amount, next_auth.authorization, authorization_validated: true) + assert_success capture + end + + def test_successful_auth_and_capture_with_normalized_recurring_stored_credential + stored_credential_params = stored_credential(:initial, :recurring, :merchant) + + assert auth = @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) + assert_success auth + assert auth.authorization + assert auth.params['scheme_response'] + assert auth.params['transaction_identifier'] + + assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) + assert_success capture + + @options[:order_id] = generate_unique_id + @options[:stored_credential] = stored_credential(:used, :recurring, :merchant, network_transaction_id: auth.params['transaction_identifier']) assert next_auth = @gateway.authorize(@amount, @credit_card, @options) assert next_auth.authorization @@ -535,14 +600,34 @@ def test_successful_auth_and_capture_with_gateway_specific_stored_credentials assert_success capture end + def test_successful_auth_and_capture_with_gateway_specific_recurring_stored_credentials + assert auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential_usage: 'FIRST', stored_credential_initiated_reason: 'RECURRING')) + assert_success auth + assert auth.authorization + assert auth.params['scheme_response'] + assert auth.params['transaction_identifier'] + + assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) + assert_success capture + + options = @options.merge( + order_id: generate_unique_id, + stored_credential_usage: 'USED', + stored_credential_initiated_reason: 'RECURRING', + stored_credential_transaction_id: auth.params['transaction_identifier'] + ) + assert next_auth = @gateway.authorize(@amount, @credit_card, options) + assert next_auth.authorization + assert next_auth.params['scheme_response'] + assert next_auth.params['transaction_identifier'] + + assert capture = @gateway.capture(@amount, next_auth.authorization, authorization_validated: true) + assert_success capture + end + def test_successful_authorize_with_3ds_with_normalized_stored_credentials session_id = generate_unique_id - stored_credential_params = { - initial_transaction: true, - reason_type: 'unscheduled', - initiator: 'merchant', - network_transaction_id: nil - } + stored_credential_params = stored_credential(:initial, :unscheduled, :merchant) options = @options.merge( { execute_threed: true, @@ -557,8 +642,7 @@ def test_successful_authorize_with_3ds_with_normalized_stored_credentials assert first_message = @gateway.authorize(@amount, @threeDS_card, options) assert first_message.test? refute first_message.authorization.blank? - refute first_message.params['issuer_url'].blank? - refute first_message.params['pa_request'].blank? + assert first_message.success? refute first_message.params['cookie'].blank? refute first_message.params['session_id'].blank? end @@ -579,8 +663,7 @@ def test_successful_authorize_with_3ds_with_gateway_specific_stored_credentials assert first_message = @gateway.authorize(@amount, @threeDS_card, options) assert first_message.test? refute first_message.authorization.blank? - refute first_message.params['issuer_url'].blank? - refute first_message.params['pa_request'].blank? + assert first_message.success? refute first_message.params['cookie'].blank? refute first_message.params['session_id'].blank? end @@ -593,7 +676,7 @@ def test_successful_purchase_with_level_two_fields end def test_successful_purchase_with_level_two_fields_and_sales_tax_zero - @level_two_data[:level_2_data][:sales_tax][:amount] = 0 + @level_two_data[:level_2_data][:tax_amount] = 0 assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_two_data)) assert_success response assert_equal true, response.params['ok'] @@ -608,12 +691,13 @@ def test_successful_purchase_with_level_three_fields end def test_unsuccessful_purchase_level_three_data_without_item_mastercard - @level_three_data[:level_3_data][:item] = {} + @level_three_data[:level_3_data][:line_items] = [{ + }] @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?).' + assert_equal response.params['error'].gsub(/\"+/, ''), 'The content of element type item must match (description,productCode?,commodityCode?,quantity?,unitCost?,unitOfMeasure?,itemTotal?,itemTotalWithTax?,itemDiscountAmount?,itemTaxRate?,lineDiscountIndicator?,itemLocalTaxRate?,itemLocalTaxAmount?,taxAmount?,categories?,pageURL?,imageURL?).' end def test_successful_purchase_with_level_two_and_three_fields @@ -837,32 +921,35 @@ def test_successful_mastercard_credit_on_cft_gateway assert_equal 'SUCCESS', credit.message end - def test_successful_fast_fund_credit_on_cft_gateway - options = @options.merge({ fast_fund_credit: true }) + # These three fast_fund_credit tests are currently failing with the message: Disbursement transaction not supported + # It seems that the current sandbox setup does not support testing this. - credit = @cftgateway.credit(@amount, @credit_card, options) - assert_success credit - assert_equal 'SUCCESS', credit.message - end + # def test_successful_fast_fund_credit_on_cft_gateway + # options = @options.merge({ fast_fund_credit: true }) - def test_successful_fast_fund_credit_with_token_on_cft_gateway - assert store = @gateway.store(@credit_card, @store_options) - assert_success store + # credit = @cftgateway.credit(@amount, @credit_card, options) + # assert_success credit + # assert_equal 'SUCCESS', credit.message + # end - options = @options.merge({ fast_fund_credit: true }) - assert credit = @cftgateway.credit(@amount, store.authorization, options) - assert_success credit - end + # def test_successful_fast_fund_credit_with_token_on_cft_gateway + # assert store = @gateway.store(@credit_card, @store_options) + # assert_success store - def test_failed_fast_fund_credit_on_cft_gateway - options = @options.merge({ fast_fund_credit: true }) - refused_card = credit_card('4917300800000000', name: 'REFUSED') # 'magic' value for testing failures, provided by Worldpay + # options = @options.merge({ fast_fund_credit: true }) + # assert credit = @cftgateway.credit(@amount, store.authorization, options) + # assert_success credit + # end - credit = @cftgateway.credit(@amount, refused_card, options) - assert_failure credit - assert_equal '01', credit.params['action_code'] - assert_equal "A transaction status of 'ok' or 'PUSH_APPROVED' is required.", credit.message - end + # def test_failed_fast_fund_credit_on_cft_gateway + # options = @options.merge({ fast_fund_credit: true }) + # refused_card = credit_card('4444333322221111', name: 'REFUSED') # 'magic' value for testing failures, provided by Worldpay + + # credit = @cftgateway.credit(@amount, refused_card, options) + # assert_failure credit + # assert_equal '01', credit.params['action_code'] + # assert_equal "A transaction status of 'ok' or 'PUSH_APPROVED' is required.", credit.message + # end def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @@ -1144,12 +1231,7 @@ def test_failed_refund_synchronous_response def test_successful_purchase_with_options_synchronous_response options = @options - stored_credential_params = { - initial_transaction: true, - reason_type: 'unscheduled', - initiator: 'merchant', - network_transaction_id: nil - } + stored_credential_params = stored_credential(:initial, :unscheduled, :merchant) options.merge(stored_credential: stored_credential_params) assert purchase = @cftgateway.purchase(@amount, @credit_card, options.merge(instalments: 3, skip_capture: true, authorization_validated: true)) diff --git a/test/remote/gateways/remote_xpay_test.rb b/test/remote/gateways/remote_xpay_test.rb new file mode 100644 index 00000000000..99999e14243 --- /dev/null +++ b/test/remote/gateways/remote_xpay_test.rb @@ -0,0 +1,46 @@ +require 'test_helper' + +class RemoteXpayTest < Test::Unit::TestCase + def setup + @gateway = XpayGateway.new(fixtures(:xpay)) + @amount = 100 + @credit_card = credit_card( + '5186151650005008', + month: 12, + year: 2026, + verification_value: '123', + brand: 'master' + ) + + @options = { + order_id: SecureRandom.alphanumeric(10), + email: 'example@example.com', + billing_address: address, + order: { + currency: 'EUR', + amount: @amount + } + } + end + + ## Test for authorization, capture, purchase and refund requires set up through 3ds + ## The only test that does not depend on a 3ds flow is verify + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match 'EXECUTED', response.message + end + + def test_successful_preauth + response = @gateway.preauth(@amount, @credit_card, @options) + assert_success response + assert_match 'PENDING', response.message + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match '400', response.error_code + assert_match 'An internal error occurred', response.message + end +end diff --git a/test/schema/orbital/Request_PTI95.xsd b/test/schema/orbital/Request_PTI95.xsd new file mode 100644 index 00000000000..53cfb98d203 --- /dev/null +++ b/test/schema/orbital/Request_PTI95.xsd @@ -0,0 +1,1396 @@ + + + + + Top level element for all XML request transaction types + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + New order Transaction Types + + + + + + Auth Only No Capture + + + + + Auth and Capture + + + + + Force Auth No Capture and no online authorization + + + + + Force Auth No Capture and no online authorization + + + + + Force Auth and Capture no online authorization + + + + + Refund and Capture no online authorization + + + + + + + New order Industry Types + + + + + + Ecommerce transaction + + + + + Recurring Payment transaction + + + + + Mail Order Telephone Order transaction + + + + + Interactive Voice Response + + + + + Interactive Voice Response + + + + + + + + + + + + Tax not provided + + + + + Tax included + + + + + Non-taxable transaction + + + + + + + + + + + + + + + + + Stratus + + + + + Tandam + + + + + + + + + + + + No mapping to order data + + + + + Use customer reference for OrderID + + + + + Use customer reference for both Order Id and Order Description + + + + + Use customer reference for Order Description + + + + + + + + + + + + + + + + + + + Auto Generate the CustomerRefNum + + + + + Use OrderID as the CustomerRefNum + + + + + Use CustomerRefNum Element + + + + + Use the description as the CustomerRefNum + + + + + Ignore. We will Ignore this entry if it's passed in the XML + + + + + + + + + + + + + + + + + + + American Express + + + + + Carte Blanche + + + + + Diners Club + + + + + Discover + + + + + GE Twinpay Credit + + + + + GECC Private Label Credit + + + + + JCB + + + + + Mastercard + + + + + Visa + + + + + GE Twinpay Debit + + + + + Switch / Solo + + + + + Electronic Check + + + + + Flex Cache + + + + + European Direct Debit + + + + + Bill Me Later + + + + + PINLess Debit + + + + + International Maestro + + + + + ChaseNet Credit + + + + + ChaseNet Signature Debit + + + + + Gap CoBrand for Visa + + + + + Interac InApp + + + + + + + + + + + + + + + + + + + Credit Card + + + + + Swith/Solo + + + + + Electronic Check + + + + + PINLess Debit + + + + + European Direct Debit + + + + + International Maestro + + + + + Chasenet Credit + + + + + Chasenet Signature Debit + + + + + Auto Assign + + + + + Use Token as Account Number + + + + + + + + + + + + + + + + + + + United States + + + + + Canada + + + + + Germany + + + + + Great Britain + + + + + + + + + + + + + + + + + Yes + + + + + Yes + + + + + No + + + + + No + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + First Recurring Transaction + + + + + Subsequent Recurring Transactions + + + + + First Installment Transaction + + + + + Subsequent Installment Transactions + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test_helper.rb b/test/test_helper.rb index ab8a5b251ba..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 = {}) @@ -270,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) @@ -294,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/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 eb4dfcf3a9c..0b0690bf248 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 @@ -172,8 +172,24 @@ def test_should_detect_forbrugsforeningen assert_equal 'forbrugsforeningen', CreditCard.brand?('6007221000000000') end - def test_should_detect_sodexo_card + def test_should_detect_sodexo_card_with_six_digits assert_equal 'sodexo', CreditCard.brand?('6060694495764400') + assert_equal 'sodexo', CreditCard.brand?('6060714495764400') + assert_equal 'sodexo', CreditCard.brand?('6033894495764400') + assert_equal 'sodexo', CreditCard.brand?('6060704495764400') + assert_equal 'sodexo', CreditCard.brand?('6060684495764400') + assert_equal 'sodexo', CreditCard.brand?('6008184495764400') + assert_equal 'sodexo', CreditCard.brand?('5058644495764400') + assert_equal 'sodexo', CreditCard.brand?('5058654495764400') + end + + def test_should_detect_sodexo_card_with_eight_digits + assert_equal 'sodexo', CreditCard.brand?('6060760195764400') + assert_equal 'sodexo', CreditCard.brand?('6060760795764400') + assert_equal 'sodexo', CreditCard.brand?('6089440095764400') + assert_equal 'sodexo', CreditCard.brand?('6089441095764400') + assert_equal 'sodexo', CreditCard.brand?('6089442095764400') + assert_equal 'sodexo', CreditCard.brand?('6060760695764400') end def test_should_detect_alia_card @@ -363,6 +379,7 @@ def test_should_detect_cabal_card 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 @@ -374,6 +391,7 @@ def test_should_detect_unionpay_card assert_equal 'unionpay', CreditCard.brand?('8171999927660000') assert_equal 'unionpay', CreditCard.brand?('8171999900000000021') assert_equal 'unionpay', CreditCard.brand?('6200000000000005') + assert_equal 'unionpay', CreditCard.brand?('6217857000000000') end def test_should_detect_synchrony_card @@ -385,6 +403,7 @@ def test_should_detect_routex_card assert_equal 'routex', CreditCard.brand?(number) assert CreditCard.valid_number?(number) assert_equal 'routex', CreditCard.brand?('7006789224703725591') + assert_equal 'routex', CreditCard.brand?('7006740000000000013') end def test_should_detect_when_an_argument_brand_does_not_match_calculated_brand @@ -502,6 +521,74 @@ 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_should_detect_tuya_card + assert_equal 'tuya', CreditCard.brand?('5888000000000000') + end + + def test_should_validate_tuya_card + assert_true CreditCard.valid_number?('5888001211111111') + # numbers with invalid formats + assert_false CreditCard.valid_number?('5888_0000_0000_0030') + end + + def test_should_detect_uatp_card_brand + assert_equal 'uatp', CreditCard.brand?('117500000000000') + assert_equal 'uatp', CreditCard.brand?('117515279008103') + assert_equal 'uatp', CreditCard.brand?('129001000000000') + end + + def test_should_validate_uatp_card + assert_true CreditCard.valid_number?('117515279008103') + assert_true CreditCard.valid_number?('116901000000000') + assert_true CreditCard.valid_number?('195724000000000') + assert_true CreditCard.valid_number?('192004000000000') + assert_true CreditCard.valid_number?('135410014004955') + end + + def test_should_detect_invalid_uatp_card + assert_false CreditCard.valid_number?('117515279008104') + assert_false CreditCard.valid_number?('116901000000001') + assert_false CreditCard.valid_number?('195724000000001') + assert_false CreditCard.valid_number?('192004000000001') + 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 e592bef4af6..595b3698bfa 100644 --- a/test/unit/credit_card_test.rb +++ b/test/unit/credit_card_test.rb @@ -174,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 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 388ef6830e0..28a766f6ca6 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 @@ -168,10 +180,18 @@ def test_failed_authorize_with_unexpected_3ds assert_match 'Received unexpected 3DS authentication response, but a 3DS initiation flag was not included in the request.', response.message end + def test_failed_authorize_with_unexpected_3ds_with_flag_ignore_threed_dynamic + @gateway.expects(:ssl_post).returns(successful_authorize_with_3ds_response) + response = @gateway.authorize(@amount, @three_ds_enrolled_card, @options.merge!(threed_dynamic: true, ignore_threed_dynamic: true)) + assert_failure response + 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 @@ -324,6 +344,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') @@ -340,6 +387,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) @@ -662,6 +717,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')) @@ -725,6 +804,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') @@ -910,6 +1012,23 @@ def test_failed_avs_check_returns_refusal_reason_raw response = @gateway.authorize(@amount, @credit_card, @options) assert_failure response assert_equal 'Refused | 05 : Do not honor', response.message + assert_equal '05', response.error_code + end + + def test_failed_without_refusal_reason_raw + @gateway.expects(:ssl_post).returns(failed_without_raw_refusal_reason) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal 'Your money is no good here', response.error_code + end + + def test_failed_without_refusal_reason + @gateway.expects(:ssl_post).returns(failed_without_refusal_reason) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_nil response.error_code end def test_scrub @@ -929,14 +1048,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 @@ -964,6 +1085,52 @@ def test_add_address assert_equal @options[:shipping_address][:country], post[:deliveryAddress][:country] end + def test_default_billing_address_country + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ + billing_address: { + address1: 'Infinite Loop', + address2: 1, + country: '', + city: 'Cupertino', + state: 'CA', + zip: '95014' + } + })) + end.check_request do |_endpoint, data, _headers| + assert_match(/"country":"ZZ"/, data) + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_default_shipping_address_country + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ + shipping_address: { + address1: 'Infinite Loop', + address2: 1, + country: '', + city: 'Cupertino', + state: 'CA', + zip: '95014' + } + })) + end.check_request do |_endpoint, data, _headers| + assert_match(/"country":"ZZ"/, data) + end.respond_with(successful_authorize_response) + assert_success response + 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 @@ -1282,6 +1449,98 @@ def test_level_3_data 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', mcc: '1234')) + end.check_request do |_endpoint, data, _headers| + assert_equal JSON.parse(data)['store'], 'test store' + assert_equal JSON.parse(data)['mcc'], '1234' + end.respond_with(successful_authorize_response) + assert_success response + end + def test_extended_avs_response response = stub_comms do @gateway.verify(@credit_card, @options) @@ -1307,6 +1566,26 @@ def test_three_decimal_places_currency_handling end end + def test_metadata_sent_through_in_authorize + metadata = { + field_one: 'A', + field_two: 'B', + field_three: 'C', + field_four: 'EASY AS ONE TWO THREE' + } + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(metadata: metadata)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal parsed['metadata']['field_one'], metadata[:field_one] + assert_equal parsed['metadata']['field_two'], metadata[:field_two] + assert_equal parsed['metadata']['field_three'], metadata[:field_three] + assert_equal parsed['metadata']['field_four'], metadata[:field_four] + end.respond_with(successful_authorize_response) + assert_success response + end + private def stored_credential_options(*args, ntid: nil) @@ -1623,6 +1902,63 @@ 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_without_raw_refusal_reason + <<-RESPONSE + { + "additionalData": + { + "refusalReasonRaw": null + }, + "refusalReason": "Your money is no good here", + "pspReference":"8514775559925128", + "resultCode":"Refused" + } + RESPONSE + end + + def failed_without_refusal_reason + <<-RESPONSE + { + "additionalData": + { + "refusalReasonRaw": null + }, + "refusalReason": null, + "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 { @@ -1672,6 +2008,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 { diff --git a/test/unit/gateways/airwallex_test.rb b/test/unit/gateways/airwallex_test.rb index ade541ce88e..6ad38180082 100644 --- a/test/unit/gateways/airwallex_test.rb +++ b/test/unit/gateways/airwallex_test.rb @@ -1,20 +1,10 @@ require 'test_helper' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - class AirwallexGateway - def setup_access_token - '12345678' - end - end - end -end - class AirwallexTest < Test::Unit::TestCase include CommStub def setup - @gateway = AirwallexGateway.new(client_id: 'login', client_api_key: 'password') + @gateway = AirwallexGateway.new(client_id: 'login', client_api_key: 'password', access_token: '12345678') @credit_card = credit_card @declined_card = credit_card('2223 0000 1018 1375') @amount = 100 @@ -28,6 +18,15 @@ def setup @stored_credential_mit_options = { initial_transaction: false, initiator: 'merchant', reason_type: 'recurring' } end + def test_setup_access_token_should_rise_an_exception_under_unauthorized + error = assert_raises(ActiveMerchant::OAuthResponseError) do + @gateway.expects(:ssl_post).returns({ code: 'invalid_argument', message: "Failed to convert 'YOUR_CLIENT_ID' to UUID", source: '' }.to_json) + @gateway.send(:setup_access_token) + end + + assert_match(/Failed to convert 'YOUR_CLIENT_ID' to UUID/, error.message) + end + def test_gateway_has_access_token assert @gateway.instance_variable_defined?(:@access_token) end diff --git a/test/unit/gateways/alelo_test.rb b/test/unit/gateways/alelo_test.rb index 3e6c9f0c1c9..a900ecda9d5 100644 --- a/test/unit/gateways/alelo_test.rb +++ b/test/unit/gateways/alelo_test.rb @@ -19,6 +19,15 @@ def setup } end + def test_fetch_access_token_should_rise_an_exception_under_unauthorized + error = assert_raises(ActiveMerchant::OAuthResponseError) do + @gateway.expects(:raw_ssl_request).returns(Net::HTTPBadRequest.new(1.0, 401, 'Unauthorized')) + @gateway.send(:fetch_access_token) + end + + assert_match(/Failed with 401 Unauthorized/, error.message) + end + def test_required_client_id_and_client_secret error = assert_raises ArgumentError do AleloGateway.new 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 16aa5f4a4a6..bfbada91eb2 100644 --- a/test/unit/gateways/authorize_net_test.rb +++ b/test/unit/gateways/authorize_net_test.rb @@ -16,11 +16,15 @@ def setup @amount = 100 @credit_card = credit_card @check = check - @apple_pay_payment_token = ActiveMerchant::Billing::ApplePayPaymentToken.new( - { data: 'encoded_payment_data' }, - payment_instrument_name: 'SomeBank Visa', - payment_network: 'Visa', - transaction_identifier: 'transaction123' + @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 = { @@ -153,7 +157,7 @@ def test_device_type_used_from_options_if_included_with_valid_track_data end def test_market_type_not_included_for_apple_pay_or_echeck - [@check, @apple_pay_payment_token].each do |payment| + [@check, @payment_token].each do |payment| stub_comms do @gateway.purchase(@amount, payment) end.check_request do |_endpoint, data, _headers| @@ -265,12 +269,10 @@ def test_failed_echeck_authorization def test_successful_apple_pay_authorization response = stub_comms do - @gateway.authorize(@amount, @apple_pay_payment_token) + @gateway.authorize(@amount, @payment_token) end.check_request do |_endpoint, data, _headers| - parse(data) do |doc| - assert_equal @gateway.class::APPLE_PAY_DATA_DESCRIPTOR, doc.at_xpath('//opaqueData/dataDescriptor').content - assert_equal Base64.strict_encode64(@apple_pay_payment_token.payment_data.to_json), doc.at_xpath('//opaqueData/dataValue').content - end + assert_match(/true<\/isPaymentToken>/, data) + assert_no_match(//, data) end.respond_with(successful_authorize_response) assert response @@ -281,12 +283,10 @@ def test_successful_apple_pay_authorization def test_successful_apple_pay_purchase response = stub_comms do - @gateway.purchase(@amount, @apple_pay_payment_token) + @gateway.purchase(@amount, @payment_token, {}) end.check_request do |_endpoint, data, _headers| - parse(data) do |doc| - assert_equal @gateway.class::APPLE_PAY_DATA_DESCRIPTOR, doc.at_xpath('//opaqueData/dataDescriptor').content - assert_equal Base64.strict_encode64(@apple_pay_payment_token.payment_data.to_json), doc.at_xpath('//opaqueData/dataValue').content - end + assert_match(/true<\/isPaymentToken>/, data) + assert_no_match(//, data) end.respond_with(successful_purchase_response) assert response @@ -367,6 +367,21 @@ def test_passes_header_email_receipt end.respond_with(successful_purchase_response) end + def test_passes_surcharge + options = @options.merge(surcharge: { + amount: 20, + description: 'test description' + }) + stub_comms do + @gateway.purchase(@amount, credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(//, data) + assert_match(/0.20<\/amount>/, data) + assert_match(/#{options[:surcharge][:description]}<\/description>/, data) + assert_match(/<\/surcharge>/, data) + end.respond_with(successful_purchase_response) + end + def test_passes_level_3_options stub_comms do @gateway.purchase(@amount, credit_card, @options.merge(@level_3_options)) @@ -940,6 +955,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) @@ -1114,6 +1143,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') @@ -1290,9 +1333,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, @@ -1313,9 +1354,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) @@ -1338,9 +1377,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, @@ -1412,8 +1449,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) @@ -1431,8 +1467,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) 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/beanstream_test.rb b/test/unit/gateways/beanstream_test.rb index 886a4479e1c..fa9f748c9ff 100644 --- a/test/unit/gateways/beanstream_test.rb +++ b/test/unit/gateways/beanstream_test.rb @@ -302,6 +302,19 @@ def test_sends_email_without_addresses assert_success response end + def test_sends_alternate_phone_number_value + @options[:billing_address][:phone] = nil + @options[:billing_address][:phone_number] = '9191234567' + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/ordPhoneNumber=9191234567/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_transcript_scrubbing assert_equal scrubbed_transcript, @gateway.scrub(transcript) end 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 8f592125771..8553f4e9aea 100644 --- a/test/unit/gateways/blue_snap_test.rb +++ b/test/unit/gateways/blue_snap_test.rb @@ -343,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) 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 fbe77e3595c..8550b24eb79 100644 --- a/test/unit/gateways/borgun_test.rb +++ b/test/unit/gateways/borgun_test.rb @@ -56,12 +56,27 @@ 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({ merchant_return_url: 'http://localhost/index.html', apply_3d_secure: '1', sale_description: 'product description' })) + @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[:merchant_return_url]}/, data) + 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 @@ -69,6 +84,7 @@ def test_successful_preauth_3ds 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 @@ -76,6 +92,7 @@ def test_successful_purchase_after_3ds @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 @@ -367,6 +384,25 @@ def successful_get_3ds_authentication_response 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 af00505e227..be9edb8ffc0 100644 --- a/test/unit/gateways/braintree_blue_test.rb +++ b/test/unit/gateways/braintree_blue_test.rb @@ -138,7 +138,15 @@ def test_verify_bad_credentials end def test_zero_dollar_verification_transaction + @gateway = BraintreeBlueGateway.new( + merchant_id: 'test', + merchant_account_id: 'present', + public_key: 'test', + private_key: 'test' + ) + Braintree::CreditCardVerificationGateway.any_instance.expects(:create). + with(has_entries(options: { merchant_account_id: 'present' })). returns(braintree_result(cvv_response_code: 'M', avs_error_response_code: 'P')) card = credit_card('4111111111111111') @@ -264,6 +272,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) @@ -737,21 +754,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 @@ -936,14 +966,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 @@ -955,13 +988,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 @@ -984,17 +1020,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', @@ -1016,18 +1054,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', @@ -1036,25 +1076,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 @@ -1150,6 +1189,22 @@ def test_stored_credential_recurring_cit_used @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: stored_credential(:cardholder, :recurring, id: '123ABC') }) end + def test_stored_credential_prefers_options_for_ntid + Braintree::TransactionGateway.any_instance.expects(:sale).with( + standard_purchase_params.merge( + { + external_vault: { + status: 'vaulted', + previous_network_transaction_id: '321XYZ' + }, + transaction_source: '' + } + ) + ).returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', network_transaction_id: '321XYZ', stored_credential: stored_credential(:cardholder, :recurring, id: '123ABC') }) + end + def test_stored_credential_recurring_mit_initial Braintree::TransactionGateway.any_instance.expects(:sale).with( standard_purchase_params.merge( @@ -1320,6 +1375,21 @@ def test_stored_credential_recurring_first_cit_initial @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: { initiator: 'merchant', reason_type: 'recurring_first', initial_transaction: true } }) end + def test_stored_credential_v2_recurring_first_cit_initial + Braintree::TransactionGateway.any_instance.expects(:sale).with( + standard_purchase_params.merge( + { + external_vault: { + status: 'will_vault' + }, + transaction_source: 'recurring_first' + } + ) + ).returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: { initiator: 'merchant', reason_type: 'recurring_first', initial_transaction: true } }) + end + def test_stored_credential_moto_cit_initial Braintree::TransactionGateway.any_instance.expects(:sale).with( standard_purchase_params.merge( @@ -1335,6 +1405,98 @@ def test_stored_credential_moto_cit_initial @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: { initiator: 'merchant', reason_type: 'moto', initial_transaction: true } }) end + def test_stored_credential_v2_recurring_first + Braintree::TransactionGateway.any_instance.expects(:sale).with( + standard_purchase_params.merge( + { + external_vault: { + status: 'will_vault' + }, + transaction_source: 'recurring_first' + } + ) + ).returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: stored_credential(:cardholder, :recurring, :initial) }) + end + + def test_stored_credential_v2_follow_on_recurring_first + Braintree::TransactionGateway.any_instance.expects(:sale).with( + standard_purchase_params.merge( + { + external_vault: { + status: 'vaulted', + previous_network_transaction_id: '123ABC' + }, + transaction_source: 'recurring' + } + ) + ).returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: stored_credential(:merchant, :recurring, id: '123ABC') }) + end + + def test_stored_credential_v2_installment_first + Braintree::TransactionGateway.any_instance.expects(:sale).with( + standard_purchase_params.merge( + { + external_vault: { + status: 'will_vault' + }, + transaction_source: 'installment_first' + } + ) + ).returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: stored_credential(:cardholder, :installment, :initial) }) + end + + def test_stored_credential_v2_follow_on_installment_first + Braintree::TransactionGateway.any_instance.expects(:sale).with( + standard_purchase_params.merge( + { + external_vault: { + status: 'vaulted', + previous_network_transaction_id: '123ABC' + }, + transaction_source: 'installment' + } + ) + ).returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: stored_credential(:merchant, :installment, id: '123ABC') }) + end + + def test_stored_credential_v2_unscheduled_cit_initial + Braintree::TransactionGateway.any_instance.expects(:sale).with( + standard_purchase_params.merge( + { + external_vault: { + status: 'will_vault' + }, + transaction_source: '' + } + ) + ).returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: stored_credential(:cardholder, :unscheduled, :initial) }) + end + + def test_stored_credential_v2_unscheduled_mit_initial + Braintree::TransactionGateway.any_instance.expects(:sale).with( + standard_purchase_params.merge( + { + external_vault: { + status: 'will_vault' + }, + transaction_source: 'unscheduled' + } + ) + ).returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: stored_credential(:merchant, :unscheduled, :initial) }) + end + def test_raises_exeption_when_adding_bank_account_to_customer_without_billing_address bank_account = check({ account_number: '1000000002', routing_number: '011000015' }) @@ -1350,6 +1512,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: { @@ -1371,6 +1541,15 @@ def test_scrub_sensitive_data assert_equal filtered_success_token_nonce, @gateway.scrub(success_create_token_nonce) end + def test_setup_purchase + Braintree::ClientTokenGateway.any_instance.expects(:generate).with do |params| + (params[:merchant_account_id] == 'merchant_account_id') + end.returns('client_token') + + response = @gateway.setup_purchase(merchant_account_id: 'merchant_account_id') + assert_equal 'client_token', response.params['client_token'] + end + private def braintree_result(options = {}) diff --git a/test/unit/gateways/braintree_token_nonce_test.rb b/test/unit/gateways/braintree_token_nonce_test.rb new file mode 100644 index 00000000000..89aac7612c8 --- /dev/null +++ b/test/unit/gateways/braintree_token_nonce_test.rb @@ -0,0 +1,205 @@ +require 'test_helper' + +class BraintreeTokenNonceTest < Test::Unit::TestCase + def setup + @gateway = BraintreeBlueGateway.new( + merchant_id: 'test', + public_key: 'test', + private_key: 'test', + test: true + ) + + @braintree_backend = @gateway.instance_eval { @braintree_gateway } + + @options = { + billing_address: { + name: 'Adrain', + address1: '96706 Onie Plains', + address2: '01897 Alysa Lock', + country: 'XXX', + city: 'Miami', + state: 'FL', + zip: '32191', + phone_number: '693-630-6935' + }, + ach_mandate: 'ach_mandate' + } + @generator = TokenNonce.new(@braintree_backend, @options) + @no_address_generator = TokenNonce.new(@braintree_backend, { ach_mandate: 'ach_mandate' }) + end + + def test_build_nonce_request_for_credit_card + credit_card = credit_card('4111111111111111') + response = @generator.send(:build_nonce_request, credit_card) + parse_response = JSON.parse response + assert_client_sdk_metadata(parse_response) + assert_equal normalize_graph(parse_response['query']), normalize_graph(credit_card_query) + assert_includes parse_response['variables']['input'], 'creditCard' + + credit_card_input = parse_response['variables']['input']['creditCard'] + + assert_equal credit_card_input['number'], credit_card.number + assert_equal credit_card_input['expirationYear'], credit_card.year.to_s + assert_equal credit_card_input['expirationMonth'], credit_card.month.to_s.rjust(2, '0') + assert_equal credit_card_input['cvv'], credit_card.verification_value + assert_equal credit_card_input['cardholderName'], credit_card.name + assert_billing_address_mapping(credit_card_input, credit_card) + end + + def test_build_nonce_request_for_bank_account + bank_account = check({ account_number: '4012000033330125', routing_number: '011000015' }) + response = @generator.send(:build_nonce_request, bank_account) + parse_response = JSON.parse response + assert_client_sdk_metadata(parse_response) + assert_equal normalize_graph(parse_response['query']), normalize_graph(bank_account_query) + assert_includes parse_response['variables']['input'], 'usBankAccount' + + bank_account_input = parse_response['variables']['input']['usBankAccount'] + + assert_equal bank_account_input['routingNumber'], bank_account.routing_number + assert_equal bank_account_input['accountNumber'], bank_account.account_number + assert_equal bank_account_input['accountType'], bank_account.account_type.upcase + assert_equal bank_account_input['achMandate'], @options[:ach_mandate] + + assert_billing_address_mapping(bank_account_input, bank_account) + + assert_equal bank_account_input['individualOwner']['firstName'], bank_account.first_name + assert_equal bank_account_input['individualOwner']['lastName'], bank_account.last_name + end + + def test_build_nonce_request_for_credit_card_without_address + credit_card = credit_card('4111111111111111') + response = @no_address_generator.send(:build_nonce_request, credit_card) + parse_response = JSON.parse response + assert_client_sdk_metadata(parse_response) + assert_equal normalize_graph(parse_response['query']), normalize_graph(credit_card_query) + assert_includes parse_response['variables']['input'], 'creditCard' + + credit_card_input = parse_response['variables']['input']['creditCard'] + + assert_equal credit_card_input['number'], credit_card.number + assert_equal credit_card_input['expirationYear'], credit_card.year.to_s + assert_equal credit_card_input['expirationMonth'], credit_card.month.to_s.rjust(2, '0') + assert_equal credit_card_input['cvv'], credit_card.verification_value + assert_equal credit_card_input['cardholderName'], credit_card.name + end + + def test_token_from + credit_card = credit_card(number: 4111111111111111) + c_token = @generator.send(:token_from, credit_card, token_credit_response) + assert_match(/tokencc_/, c_token) + + bakn_account = check({ account_number: '4012000033330125', routing_number: '011000015' }) + b_token = @generator.send(:token_from, bakn_account, token_bank_response) + assert_match(/tokenusbankacct_/, b_token) + end + + def test_nil_token_from + credit_card = credit_card(number: 4111111111111111) + c_token = @generator.send(:token_from, credit_card, token_bank_response) + assert_nil c_token + + bakn_account = check({ account_number: '4012000033330125', routing_number: '011000015' }) + b_token = @generator.send(:token_from, bakn_account, token_credit_response) + assert_nil b_token + end + + def assert_billing_address_mapping(request_input, payment_method) + assert_equal request_input['billingAddress']['streetAddress'], @options[:billing_address][:address1] + assert_equal request_input['billingAddress']['extendedAddress'], @options[:billing_address][:address2] + + if payment_method.is_a?(Check) + assert_equal request_input['billingAddress']['city'], @options[:billing_address][:city] + assert_equal request_input['billingAddress']['state'], @options[:billing_address][:state] + assert_equal request_input['billingAddress']['zipCode'], @options[:billing_address][:zip] + else + assert_equal request_input['billingAddress']['locality'], @options[:billing_address][:city] + assert_equal request_input['billingAddress']['region'], @options[:billing_address][:state] + assert_equal request_input['billingAddress']['postalCode'], @options[:billing_address][:zip] + end + end + + def assert_client_sdk_metadata(parse_response) + assert_equal parse_response['clientSdkMetadata']['platform'], 'web' + assert_equal parse_response['clientSdkMetadata']['source'], 'client' + assert_equal parse_response['clientSdkMetadata']['integration'], 'custom' + assert_match(/\A[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}\z/i, parse_response['clientSdkMetadata']['sessionId']) + assert_equal parse_response['clientSdkMetadata']['version'], '3.83.0' + end + + private + + def normalize_graph(graph) + graph.gsub(/\s+/, ' ').strip + end + + def bank_account_query + <<-GRAPHQL + mutation TokenizeUsBankAccount($input: TokenizeUsBankAccountInput!) { + tokenizeUsBankAccount(input: $input) { + paymentMethod { + id + details { + ... on UsBankAccountDetails { + last4 + } + } + } + } + } + GRAPHQL + end + + def credit_card_query + <<-GRAPHQL + mutation TokenizeCreditCard($input: TokenizeCreditCardInput!) { + tokenizeCreditCard(input: $input) { + paymentMethod { + id + details { + ... on CreditCardDetails { + last4 + } + } + } + } + } + GRAPHQL + end + + def token_credit_response + { + 'data' => { + 'tokenizeCreditCard' => { + 'paymentMethod' => { + 'id' => 'tokencc_bc_72n3ms_74wsn3_jp2vn4_gjj62v_g33', + 'details' => { + 'last4' => '1111' + } + } + } + }, + 'extensions' => { + 'requestId' => 'a093afbb-42a9-4a85-973f-0ca79dff9ba6' + } + } + end + + def token_bank_response + { + 'data' => { + 'tokenizeUsBankAccount' => { + 'paymentMethod' => { + 'id' => 'tokenusbankacct_bc_zrg45z_7wz95v_nscrks_q4zpjs_5m7', + 'details' => { + 'last4' => '0125' + } + } + } + }, + 'extensions' => { + 'requestId' => '769b26d5-27e4-4602-b51d-face8b6ffdd5' + } + } + end +end diff --git a/test/unit/gateways/card_stream_test.rb b/test/unit/gateways/card_stream_test.rb index 509dc81fc38..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: { @@ -51,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 diff --git a/test/unit/gateways/cecabank_rest_json_test.rb b/test/unit/gateways/cecabank_rest_json_test.rb new file mode 100644 index 00000000000..913e171e054 --- /dev/null +++ b/test/unit/gateways/cecabank_rest_json_test.rb @@ -0,0 +1,341 @@ +require 'test_helper' + +class CecabankJsonTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = CecabankJsonGateway.new( + merchant_id: '12345678', + acquirer_bin: '12345678', + terminal_id: '00000003', + cypher_key: 'enc_key', + encryption_key: '00112233445566778899AABBCCDDEEFF00001133445566778899AABBCCDDEEAA', + initiator_vector: '0000000000000000' + ) + + @credit_card = credit_card + @amex_card = credit_card('374245455400001', { month: 10, year: Time.now.year + 1, verification_value: '1234' }) + @amount = 100 + + @options = { + order_id: '1', + description: 'Store Purchase' + } + + @three_d_secure = { + version: '2.2.0', + eci: '02', + cavv: '4F80DF50ADB0F9502B91618E9B704790EABA35FDFC972DDDD0BF498C6A75E492', + ds_transaction_id: 'a2bf089f-cefc-4d2c-850f-9153827fe070', + acs_transaction_id: '18c353b0-76e3-4a4c-8033-f14fe9ce39dc', + authentication_response_status: 'Y', + three_ds_server_trans_id: '9bd9aa9c-3beb-4012-8e52-214cccb25ec5' + } + end + + def test_successful_authorize + @gateway.expects(:ssl_request).returns(successful_authorize_response) + + assert response = @gateway.authorize(@amount, @credit_card, @options) + assert_instance_of Response, response + assert_success response + assert_equal '12004172282310181802446007000#1#100', response.authorization + assert response.test? + end + + def test_failed_authorize + @gateway.expects(:ssl_request).returns(failed_authorize_response) + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_failure response + assert_equal '27', response.error_code + end + + def test_successful_capture + @gateway.expects(:ssl_request).returns(successful_capture_response) + + assert response = @gateway.capture(@amount, 'reference', @options) + assert_instance_of Response, response + assert_success response + assert_equal '12204172322310181826516007000#1#100', response.authorization + assert response.test? + end + + def test_failed_capture + @gateway.expects(:ssl_request).returns(failed_capture_response) + response = @gateway.capture(@amount, 'reference', @options) + + assert_failure response + assert_equal '807', response.error_code + end + + def test_successful_purchase + @gateway.expects(:ssl_request).returns(successful_purchase_response) + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_instance_of Response, response + assert_success response + assert_equal '12004172192310181720006007000#1#100', response.authorization + assert response.test? + end + + def test_successful_stored_credentials_with_network_transaction_id_as_gsf + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + @options.merge!({ network_transaction_id: '12345678901234567890' }) + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_instance_of Response, response + assert_success response + assert_equal '12004172192310181720006007000#1#100', response.authorization + assert response.test? + end + + def test_failed_purchase + @gateway.expects(:ssl_request).returns(failed_purchase_response) + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_equal '27', response.error_code + end + + def test_successful_refund + @gateway.expects(:ssl_request).returns(successful_refund_response) + + assert response = @gateway.refund(@amount, 'reference', @options) + assert_instance_of Response, response + assert_success response + assert_equal '12204172352310181847426007000#1#100', response.authorization + assert response.test? + end + + def test_failed_refund + @gateway.expects(:ssl_request).returns(failed_refund_response) + + assert response = @gateway.refund(@amount, 'reference', @options) + assert_failure response + assert response.test? + end + + def test_successful_void + @gateway.expects(:ssl_request).returns(successful_void_response) + + assert response = @gateway.void('12204172352310181847426007000#1#10', @options) + assert_instance_of Response, response + assert_success response + assert_equal '14204172402310181906166007000#1#10', response.authorization + assert response.test? + end + + def test_failed_void + @gateway.expects(:ssl_request).returns(failed_void_response) + + assert response = @gateway.void('reference', @options) + assert_failure response + assert response.test? + end + + def test_purchase_without_exemption_type + @options[:exemption_type] = nil + @options[:three_d_secure] = @three_d_secure + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + data = JSON.parse(data) + params = JSON.parse(Base64.decode64(data['parametros'])) + three_d_secure = JSON.parse(params['ThreeDsResponse']) + assert_nil three_d_secure['exemption_type'] + assert_match params['exencionSCA'], 'NONE' + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_low_value_exemption + @options[:exemption_type] = 'low_value_exemption' + @options[:three_d_secure] = @three_d_secure + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + data = JSON.parse(data) + params = JSON.parse(Base64.decode64(data['parametros'])) + three_d_secure = JSON.parse(params['ThreeDsResponse']) + assert_match three_d_secure['exemption_type'], 'low_value_exemption' + assert_match params['exencionSCA'], 'LOW' + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_transaction_risk_analysis_exemption + @options[:exemption_type] = 'transaction_risk_analysis_exemption' + @options[:three_d_secure] = @three_d_secure + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + data = JSON.parse(data) + params = JSON.parse(Base64.decode64(data['parametros'])) + three_d_secure = JSON.parse(params['ThreeDsResponse']) + assert_match three_d_secure['exemption_type'], 'transaction_risk_analysis_exemption' + assert_match params['exencionSCA'], 'TRA' + end.respond_with(successful_purchase_response) + end + + def test_purchase_without_threed_secure_data + @options[:three_d_secure] = nil + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + data = JSON.parse(data) + params = JSON.parse(Base64.decode64(data['parametros'])) + assert_nil params['ThreeDsResponse'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_for_amex_include_correct_verification_value + stub_comms do + @gateway.purchase(@amount, @amex_card, @options) + end.check_request do |_endpoint, data, _headers| + data = JSON.parse(data) + params = JSON.parse(Base64.decode64(data['parametros'])) + credit_card_data = decrypt_sensitive_fields(@gateway.options, params['encryptedData']) + amex_card = JSON.parse(credit_card_data) + assert_nil amex_card['cvv2'] + assert_equal amex_card['csc'], '1234' + end.respond_with(successful_purchase_response) + end + + def test_transcript_scrubbing + assert_equal scrubbed_transcript, @gateway.scrub(transcript) + end + + private + + def decrypt_sensitive_fields(options, data) + cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt + cipher.key = [options[:encryption_key]].pack('H*') + cipher.iv = options[:initiator_vector]&.split('')&.map(&:to_i)&.pack('c*') + cipher.update([data].pack('H*')) + cipher.final + end + + def transcript + <<~RESPONSE + "opening connection to tpv.ceca.es:443...\nopened\nstarting SSL for tpv.ceca.es:443...\nSSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384\n<- \"POST /tpvweb/rest/procesos/compra HTTP/1.1\\r\\nContent-Type: application/json\\r\\nHost: tpv.ceca.es\\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\\nContent-Length: 1397\\r\\n\\r\\n\"\n<- \"{\\\"parametros\\\":\\\"eyJhY2Npb24iOiJSRVNUX0FVVE9SSVpBQ0lPTiIsIm51bU9wZXJhY2lvbiI6ImYxZDdlNjBlMDYzMTJiNjI5NDEzOTUxM2YwMGQ2YWM4IiwiaW1wb3J0ZSI6IjEwMCIsInRpcG9Nb25lZGEiOiI5NzgiLCJleHBvbmVudGUiOiIyIiwiZW5jcnlwdGVkRGF0YSI6IjhlOWZhY2RmMDk5NDFlZTU0ZDA2ODRiNDNmNDNhMmRmOGM4ZWE5ODlmYTViYzYyOTM4ODFiYWVjNDFiYjU4OGNhNDc3MWI4OTFmNTkwMWVjMmJhZmJhOTBmMDNkM2NiZmUwNTJlYjAzMDU4Zjk1MGYyNzY4YTk3OWJiZGQxNmJlZmIyODQ2Zjc2MjkyYTFlODYzMDNhNTVhYTIzNjZkODA5MDEyYzlhNzZmYTZiOTQzOWNlNGQ3MzY5NTYwOTNhMDAwZTk5ZDMzNmVhZDgwMjBmOTk5YjVkZDkyMTFjMjE5ZWRhMjVmYjVkZDY2YzZiOTMxZWY3MjY5ZjlmMmVjZGVlYTc2MWRlMDEyZmFhMzg3MDlkODcyNTI4ODViYjI1OThmZDI2YTQzMzNhNDEwMmNmZTg4YjM1NTJjZWU0Yzc2IiwiZXhlbmNpb25TQ0EiOiJOT05FIiwiVGhyZWVEc1Jlc3BvbnNlIjoie1wiZXhlbXB0aW9uX3R5cGVcIjpudWxsLFwidGhyZWVfZHNfdmVyc2lvblwiOlwiMi4yLjBcIixcImRpcmVjdG9yeV9zZXJ2ZXJfdHJhbnNhY3Rpb25faWRcIjpcImEyYmYwODlmLWNlZmMtNGQyYy04NTBmLTkxNTM4MjdmZTA3MFwiLFwiYWNzX3RyYW5zYWN0aW9uX2lkXCI6XCIxOGMzNTNiMC03NmUzLTRhNGMtODAzMy1mMTRmZTljZTM5ZGNcIixcImF1dGhlbnRpY2F0aW9uX3Jlc3BvbnNlX3N0YXR1c1wiOlwiWVwiLFwidGhyZWVfZHNfc2VydmVyX3RyYW5zX2lkXCI6XCI5YmQ5YWE5Yy0zYmViLTQwMTItOGU1Mi0yMTRjY2NiMjVlYzVcIixcImVjb21tZXJjZV9pbmRpY2F0b3JcIjpcIjAyXCIsXCJlbnJvbGxlZFwiOm51bGwsXCJhbW91bnRcIjpcIjEwMFwifSIsIm1lcmNoYW50SUQiOiIxMDY5MDA2NDAiLCJhY3F1aXJlckJJTiI6IjAwMDA1NTQwMDAiLCJ0ZXJtaW5hbElEIjoiMDAwMDAwMDMifQ==\\\",\\\"cifrado\\\":\\\"SHA2\\\",\\\"firma\\\":\\\"ac7e5eb06b675be6c6f58487bbbaa1ddc07518e216cb0788905caffd911eea87\\\"}\"\n-> \"HTTP/1.1 200 OK\\r\\n\"\n-> \"Date: Thu, 14 Dec 2023 15:52:41 GMT\\r\\n\"\n-> \"Server: Apache\\r\\n\"\n-> \"Strict-Transport-Security: max-age=31536000; includeSubDomains\\r\\n\"\n-> \"X-XSS-Protection: 1; mode=block\\r\\n\"\n-> \"X-Content-Type-Options: nosniff\\r\\n\"\n-> \"Content-Length: 103\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"Content-Type: application/json\\r\\n\"\n-> \"\\r\\n\"\nreading 103 bytes...\n-> \"{\\\"cifrado\\\":\\\"SHA2\\\",\\\"parametros\\\":\\\"eyJudW1BdXQiOiIxMDEwMDAiLCJyZWZlcmVuY2lhIjoiMTIwMDQzOTQ4MzIzMTIxNDE2NDg0NjYwMDcwMDAiLCJjb2RBdXQiOiIwMDAifQ==\\\",\\\"firma\\\":\\\"5ce066be8892839d6aa6da15405c9be8987642f4245fac112292084a8532a538\\\",\\\"fecha\\\":\\\"231214164846089\\\",\\\"idProceso\\\":\\\"106900640-adeda8b09b84630d6247b53748ab9c66\\\"}\"\nread 300 bytes\nConn close\n" + RESPONSE + end + + def scrubbed_transcript + <<~RESPONSE + "opening connection to tpv.ceca.es:443...\nopened\nstarting SSL for tpv.ceca.es:443...\nSSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384\n<- \"POST /tpvweb/rest/procesos/compra HTTP/1.1\\r\\nContent-Type: application/json\\r\\nHost: tpv.ceca.es\\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\\nContent-Length: 1397\\r\\n\\r\\n\"\n<- \"{\\\"parametros\\\":\\\"eyJhY2Npb24iOiJSRVNUX0FVVE9SSVpBQ0lPTiIsIm51bU9wZXJhY2lvbiI6ImYxZDdlNjBlMDYzMTJiNjI5NDEzOTUxM2YwMGQ2YWM4IiwiaW1wb3J0ZSI6IjEwMCIsInRpcG9Nb25lZGEiOiI5NzgiLCJleHBvbmVudGUiOiIyIiwiZW5jcnlwdGVkRGF0YSI6ImEyZjczODJjMDdiZGYxYWZiZDE3YWJiMGQ3NTNmMzJlYmIzYTFjNGY4ZGNmMjYxZWQ2YTkxMmQ3MzlkNzE2ZjA1MDBiOTg5NzliY2I1MzY0NTRlMGE2ZmJiYzVlNjJlNjgxZjgyMTEwNGFiNjUzOTYyMjA4NmMwZGM2MzgyYWRmNjRkOGFjZWYwY2U5MDBjMzJlZmFjM2Q5YmJhM2UxZGY3NDY2NzU3NWNiYjMzYTczMDU3NGYzMzJmMGNlNTliOTU5MzM4NjQxOGUwYjIyNDJiOTJmZDg2MDczM2QxNzhiZDZkNGIyZGMwMzE2ZGRmNTAzMTQ5N2I1YWViMjRlMzQiLCJleGVuY2lvblNDQSI6Ik5PTkUiLCJUaHJlZURzUmVzcG9uc2UiOiJ7XCJleGVtcHRpb25fdHlwZVwiOm51bGwsXCJ0aHJlZV9kc192ZXJzaW9uXCI6XCIyLjIuMFwiLFwiZGlyZWN0b3J5X3NlcnZlcl90cmFuc2FjdGlvbl9pZFwiOlwiYTJiZjA4OWYtY2VmYy00ZDJjLTg1MGYtOTE1MzgyN2ZlMDcwXCIsXCJhY3NfdHJhbnNhY3Rpb25faWRcIjpcIjE4YzM1M2IwLTc2ZTMtNGE0Yy04MDMzLWYxNGZlOWNlMzlkY1wiLFwiYXV0aGVudGljYXRpb25fcmVzcG9uc2Vfc3RhdHVzXCI6XCJZXCIsXCJ0aHJlZV9kc19zZXJ2ZXJfdHJhbnNfaWRcIjpcIjliZDlhYTljLTNiZWItNDAxMi04ZTUyLTIxNGNjY2IyNWVjNVwiLFwiZWNvbW1lcmNlX2luZGljYXRvclwiOlwiMDJcIixcImVucm9sbGVkXCI6bnVsbCxcImFtb3VudFwiOlwiMTAwXCJ9IiwibWVyY2hhbnRJRCI6IjEwNjkwMDY0MCIsImFjcXVpcmVyQklOIjoiMDAwMDU1NDAwMCIsInRlcm1pbmFsSUQiOiIwMDAwMDAwMyJ9\\\",\\\"cifrado\\\":\\\"SHA2\\\",\\\"firma\\\":\\\"ac7e5eb06b675be6c6f58487bbbaa1ddc07518e216cb0788905caffd911eea87\\\"}\"\n-> \"HTTP/1.1 200 OK\\r\\n\"\n-> \"Date: Thu, 14 Dec 2023 15:52:41 GMT\\r\\n\"\n-> \"Server: Apache\\r\\n\"\n-> \"Strict-Transport-Security: max-age=31536000; includeSubDomains\\r\\n\"\n-> \"X-XSS-Protection: 1; mode=block\\r\\n\"\n-> \"X-Content-Type-Options: nosniff\\r\\n\"\n-> \"Content-Length: 103\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"Content-Type: application/json\\r\\n\"\n-> \"\\r\\n\"\nreading 103 bytes...\n-> \"{\\\"cifrado\\\":\\\"SHA2\\\",\\\"parametros\\\":\\\"eyJudW1BdXQiOiIxMDEwMDAiLCJyZWZlcmVuY2lhIjoiMTIwMDQzOTQ4MzIzMTIxNDE2NDg0NjYwMDcwMDAiLCJjb2RBdXQiOiIwMDAifQ==\\\",\\\"firma\\\":\\\"5ce066be8892839d6aa6da15405c9be8987642f4245fac112292084a8532a538\\\",\\\"fecha\\\":\\\"231214164846089\\\",\\\"idProceso\\\":\\\"106900640-adeda8b09b84630d6247b53748ab9c66\\\"}\"\nread 300 bytes\nConn close\n" + RESPONSE + end + + def successful_authorize_response + <<~RESPONSE + { + "cifrado":"SHA2", + "parametros":"eyJudW1BdXQiOiIxMDEwMDAiLCJyZWZlcmVuY2lhIjoiMTIwMDQxNzIyODIzMTAxODE4MDI0NDYwMDcwMDAiLCJjb2RBdXQiOiIwMDAifQ==", + "firma":"2271f18614f9e3bf1f1d0bde7c23d2d9b576087564fd6cb4474f14f5727eaff2", + "fecha":"231018180245479", + "idProceso":"106900640-9da0de26e0e81697f7629566b99a1b73" + } + RESPONSE + end + + def failed_authorize_response + <<~RESPONSE + { + "fecha":"231018180927186", + "idProceso":"106900640-9cfe017407164563ca5aa7a0877d2ade", + "codResult":"27" + } + RESPONSE + end + + def successful_capture_response + <<~RESPONSE + { + "cifrado":"SHA2", + "parametros":"eyJudW1BdXQiOiIxMDEwMDAiLCJyZWZlcmVuY2lhIjoiMTIyMDQxNzIzMjIzMTAxODE4MjY1MTYwMDcwMDAiLCJjb2RBdXQiOiI5MDAifQ==", + "firma":"9dead8ef2bf1f82cde1954cefaa9eca67b630effed7f71a5fd3bb3bd2e6e0808", + "fecha":"231018182651711", + "idProceso":"106900640-5b03c604fd76ecaf8715a29c482f3040" + } + RESPONSE + end + + def failed_capture_response + <<~RESPONSE + { + "fecha":"231018183020560", + "idProceso":"106900640-d0cab45d2404960b65fe02445e97b7e2", + "codResult":"807" + } + RESPONSE + end + + def successful_purchase_response + <<~RESPONSE + { + "cifrado":"SHA2", + "parametros":"eyJudW1BdXQiOiIxMDEwMDAiLCJyZWZlcmVuY2lhIjoiMTIwMDQxNzIxOTIzMTAxODE3MjAwMDYwMDcwMDAiLCJjb2RBdXQiOiIwMDAifQ==", + "firma":"da751ff809f54842ff26aed009cdce2d1a3b613cb3be579bb17af2e3ab36aa37", + "fecha":"231018172001775", + "idProceso":"106900640-bd4bd321774c51ec91cf24ca6bbca913" + } + RESPONSE + end + + def failed_purchase_response + <<~RESPONSE + { + "fecha":"231018174516102", + "idProceso":"106900640-29c9d010e2e8c33872a4194df4e7a544", + "codResult":"27" + } + RESPONSE + end + + def successful_refund_response + <<~RESPONSE + { + "cifrado":"SHA2", + "parametros":"eyJtZXJjaGFudElEIjoiMTA2OTAwNjQwIiwiYWNxdWlyZXJCSU4iOiIwMDAwNTU0MDAwIiwidGVybWluYWxJRCI6IjAwMDAwMDAzIiwibnVtT3BlcmFjaW9uIjoiOGYyOTJiYTcwMmEzMTZmODIwMmEzZGFjY2JhMjFmZWMiLCJpbXBvcnRlIjoiMTAwIiwibnVtQXV0IjoiMTAxMDAwIiwicmVmZXJlbmNpYSI6IjEyMjA0MTcyMzUyMzEwMTgxODQ3NDI2MDA3MDAwIiwidGlwb09wZXJhY2lvbiI6IkQiLCJwYWlzIjoiMDAwIiwiY29kQXV0IjoiOTAwIn0=", + "firma":"37591482e4d1dce6317c6d7de6a6c9b030c0618680eaefb4b42b0d8af3854773", + "fecha":"231018184743876", + "idProceso":"106900640-8f292ba702a316f8202a3daccba21fec" + } + RESPONSE + end + + def failed_refund_response + <<~RESPONSE + { + "fecha":"231018185809202", + "idProceso":"106900640-fc93d837dba2003ad767d682e6eb5d5f", + "codResult":"15" + } + RESPONSE + end + + def successful_void_response + <<~RESPONSE + { + "cifrado":"SHA2", + "parametros":"eyJtZXJjaGFudElEIjoiMTA2OTAwNjQwIiwiYWNxdWlyZXJCSU4iOiIwMDAwNTU0MDAwIiwidGVybWluYWxJRCI6IjAwMDAwMDAzIiwibnVtT3BlcmFjaW9uIjoiMDNlMTkwNTU4NWZlMmFjM2M4N2NiYjY4NGUyMjYwZDUiLCJpbXBvcnRlIjoiMTAwIiwibnVtQXV0IjoiMTAxMDAwIiwicmVmZXJlbmNpYSI6IjE0MjA0MTcyNDAyMzEwMTgxOTA2MTY2MDA3MDAwIiwidGlwb09wZXJhY2lvbiI6IkQiLCJwYWlzIjoiMDAwIiwiY29kQXV0IjoiNDAwIn0=", + "firma":"af55904b24cb083e6514b86456b107fdb8ebfc715aed228321ad959b13ef2b23", + "fecha":"231018190618224", + "idProceso":"106900640-03e1905585fe2ac3c87cbb684e2260d5" + } + RESPONSE + end + + def failed_void_response + <<~RESPONSE + { + "fecha":"231018191116348", + "idProceso":"106900640-d7ca10f4fae36b2ad81f330eeb1ce509", + "codResult":"15" + } + RESPONSE + end +end diff --git a/test/unit/gateways/cecabank_test.rb b/test/unit/gateways/cecabank_test.rb index 2641b1b6800..e2bfe3c749e 100644 --- a/test/unit/gateways/cecabank_test.rb +++ b/test/unit/gateways/cecabank_test.rb @@ -4,11 +4,11 @@ class CecabankTest < Test::Unit::TestCase include CommStub def setup - @gateway = CecabankGateway.new( + @gateway = CecabankXmlGateway.new( merchant_id: '12345678', acquirer_bin: '12345678', terminal_id: '00000003', - key: 'enc_key' + cypher_key: 'enc_key' ) @credit_card = credit_card diff --git a/test/unit/gateways/checkout_v2_test.rb b/test/unit/gateways/checkout_v2_test.rb index 6a04bfbcbca..1fcc42989e2 100644 --- a/test/unit/gateways/checkout_v2_test.rb +++ b/test/unit/gateways/checkout_v2_test.rb @@ -1,15 +1,5 @@ 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 @@ -17,14 +7,27 @@ def setup @gateway = CheckoutV2Gateway.new( secret_key: '1111111111111' ) - @gateway_oauth = CheckoutV2Gateway.new({ client_id: 'abcd', client_secret: '1234' }) - + @gateway_oauth = CheckoutV2Gateway.new({ client_id: 'abcd', client_secret: '1234', access_token: '12345678' }) + @gateway_api = CheckoutV2Gateway.new({ + secret_key: '1111111111111', + public_key: '2222222222222' + }) @credit_card = credit_card @amount = 100 + @token = '2MPedsuenG2o8yFfrsdOBWmOuEf' + end + + def test_setup_access_token_should_rise_an_exception_under_bad_request + error = assert_raises(ActiveMerchant::OAuthResponseError) do + @gateway.expects(:raw_ssl_request).returns(Net::HTTPBadRequest.new(1.0, 400, 'Bad Request')) + @gateway.send(:setup_access_token) + end + + assert_match(/Failed with 400 Bad Request/, error.message) 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) @@ -34,7 +37,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) @@ -45,7 +48,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) @@ -57,9 +60,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') @@ -75,18 +78,18 @@ def test_successful_purchase_using_vts_network_token_without_eci end def test_successful_passing_processing_channel_id - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, { processing_channel_id: '123456abcde' }) - end.check_request do |_endpoint, data, _headers| + 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 do + response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, @credit_card, { incremental_authorization: 'abcd1234' }) - end.check_request do |endpoint, _data, _headers| + end.check_request do |_method, endpoint, _data, _headers| assert_include endpoint, 'abcd1234' end.respond_with(successful_incremental_authorize_response) @@ -94,18 +97,18 @@ def test_successful_passing_incremental_authorization end def test_successful_passing_authorization_type - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, { authorization_type: 'Estimated' }) - end.check_request do |_endpoint, data, _headers| + 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 do + 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 |_endpoint, data, _headers| + 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') @@ -113,9 +116,9 @@ def test_successful_passing_exemption_and_challenge_indicator end def test_successful_passing_capture_type - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.capture(@amount, 'abc', { capture_type: 'NonFinal' }) - end.check_request do |_endpoint, data, _headers| + 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) @@ -126,9 +129,9 @@ def test_successful_purchase_using_vts_network_token_with_eci '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') @@ -148,9 +151,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') @@ -170,9 +173,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') @@ -192,9 +195,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') @@ -214,9 +217,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') @@ -236,9 +239,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') @@ -257,16 +260,21 @@ 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) + end.check_request do |_method, endpoint, data, headers| + if endpoint.match?(/token/) + assert_equal headers['Authorization'], 'Basic YWJjZDoxMjM0' + assert_equal data, 'grant_type=client_credentials' + else + request = JSON.parse(data) + assert_equal headers['Authorization'], 'Bearer 12345678' + assert_equal request['processing_channel_id'], processing_channel_id + end + end.respond_with(successful_access_token_response, 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) @@ -277,7 +285,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) @@ -285,43 +293,158 @@ 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_purchase_with_recipient_fields + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { + recipient: { + dob: '1985-05-15', + account_number: '5555554444', + zip: 'SW1A', + first_name: 'john', + last_name: 'johnny', + address: { + address_line1: '123 High St.', + address_line2: 'Flat 456', + city: 'London', + state: 'str', + zip: 'SW1A 1AA', + country: 'GB' + } + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r{"dob":"1985-05-15"}, data) + assert_match(%r{"account_number":"5555554444"}, data) + assert_match(%r{"zip":"SW1A"}, data) + assert_match(%r{"first_name":"john"}, data) + assert_match(%r{"last_name":"johnny"}, data) + assert_match(%r{"address_line1":"123 High St."}, data) + assert_match(%r{"address_line2":"Flat 456"}, data) + assert_match(%r{"city":"London"}, data) + assert_match(%r{"state":"str"}, data) + assert_match(%r{"zip":"SW1A 1AA"}, data) + assert_match(%r{"country":"GB"}, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_purchase_with_sender_fields + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { + sender: { + type: 'individual', + dob: '1985-05-15', + first_name: 'Jane', + last_name: 'Doe', + address: { + address1: '123 High St.', + address2: 'Flat 456', + city: 'London', + state: 'str', + zip: 'SW1A 1AA', + country: 'GB' + }, + reference: '8285282045818', + identification: { + type: 'passport', + number: 'ABC123', + issuing_country: 'GB' + } + } + }) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data)['sender'] + assert_equal request['first_name'], 'Jane' + assert_equal request['last_name'], 'Doe' + assert_equal request['type'], 'individual' + assert_equal request['dob'], '1985-05-15' + assert_equal request['reference'], '8285282045818' + assert_equal request['address']['address_line1'], '123 High St.' + assert_equal request['address']['address_line2'], 'Flat 456' + assert_equal request['address']['city'], 'London' + assert_equal request['address']['state'], 'str' + assert_equal request['address']['zip'], 'SW1A 1AA' + assert_equal request['address']['country'], 'GB' + assert_equal request['identification']['type'], 'passport' + assert_equal request['identification']['number'], 'ABC123' + assert_equal request['identification']['issuing_country'], 'GB' + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_purchase_with_processing_fields + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { + processing: { + aft: true + } + }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(%r{"aft":true}, 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 do + stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card) - end.check_request do |_endpoint, data, _headers| + 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 assert_equal Gateway::STANDARD_ERROR_CODE[:invalid_number], response.error_code end + def test_failed_purchase_3ds_with_threeds_response_message + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { execute_threed: true, exemption: 'no_preference', challenge_indicator: 'trusted_listing', threeds_response_message: true }) + end.respond_with(failed_purchase_3ds_response) + + assert_failure response + assert_equal 'Insufficient Funds', response.message + assert_equal nil, response.error_code + end + + def test_failed_purchase_3ds_without_threeds_response_message + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { execute_threed: true, exemption: 'no_preference', challenge_indicator: 'trusted_listing' }) + end.respond_with(failed_purchase_3ds_response) + + assert_failure response + assert_equal 'Declined', response.message + assert_equal nil, response.error_code + 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) @@ -329,29 +452,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) @@ -359,15 +478,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) @@ -376,7 +496,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, @@ -385,7 +505,7 @@ def test_successful_purchase_with_stored_credentials } } @gateway.purchase(@amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + 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 @@ -396,7 +516,7 @@ def test_successful_purchase_with_stored_credentials end def test_successful_purchase_with_stored_credentials_merchant_initiated_transaction_id - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do options = { stored_credential: { initial_transaction: false @@ -404,7 +524,7 @@ def test_successful_purchase_with_stored_credentials_merchant_initiated_transact merchant_initiated_transaction_id: 'pay_7jcf4ovmwnqedhtldca3fjli2y' } @gateway.purchase(@amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + 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 @@ -414,8 +534,38 @@ def test_successful_purchase_with_stored_credentials_merchant_initiated_transact 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', @@ -423,7 +573,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) @@ -431,8 +581,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', @@ -440,7 +601,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) @@ -448,7 +609,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) @@ -456,14 +617,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) @@ -471,13 +632,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) @@ -489,7 +650,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 @@ -502,7 +672,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, @@ -520,7 +690,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) @@ -528,7 +698,7 @@ 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: { @@ -545,7 +715,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) @@ -553,7 +723,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) @@ -563,7 +733,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) @@ -571,14 +741,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) @@ -586,7 +756,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', @@ -594,7 +764,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) @@ -602,7 +772,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) @@ -610,7 +780,7 @@ 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 @@ -623,9 +793,9 @@ def test_successfully_passes_fund_type_and_fields source_id: 'ca_spwmped4qmqenai7hcghquqle4', account_holder_type: 'individual' } - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.credit(@amount, @credit_card, options) - end.check_request do |_endpoint, data, _headers| + 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] @@ -637,15 +807,122 @@ def test_successfully_passes_fund_type_and_fields 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) @@ -653,7 +930,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', @@ -661,7 +938,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) @@ -669,7 +946,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) @@ -677,7 +954,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) @@ -685,7 +962,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 @@ -693,13 +970,40 @@ 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 @@ -709,7 +1013,7 @@ def test_network_transaction_scrubbing 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) @@ -718,7 +1022,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) @@ -727,7 +1031,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) @@ -739,6 +1043,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 @@ -769,12 +1125,24 @@ def post_scrubbed ) end + def successful_access_token_response + %( + {"access_token":"12345678","expires_in":3600,"token_type":"Bearer","scope":"disputes:accept disputes:provide-evidence disputes:view files flow:events flow:workflows fx gateway gateway:payment gateway:payment-authorizations gateway:payment-captures gateway:payment-details gateway:payment-refunds gateway:payment-voids middleware middleware:merchants-secret payouts:bank-details risk sessions:app sessions:browser vault:instruments"} + ) + end + def successful_purchase_response %( {"id":"pay_bgv5tmah6fmuzcmcrcro6exe6m","action_id":"act_bgv5tmah6fmuzcmcrcro6exe6m","amount":200,"currency":"USD","approved":true,"status":"Authorized","auth_code":"127172","eci":"05","scheme_id":"096091887499308","response_code":"10000","response_summary":"Approved","risk":{"flagged":false},"source":{"id":"src_fzp3cwkf4ygebbmvrxdhyrwmbm","type":"card","billing_address":{"address_line1":"456 My Street","address_line2":"Apt 1","city":"Ottawa","state":"ON","zip":"K1C2N6","country":"CA"},"expiry_month":6,"expiry_year":2025,"name":"Longbob Longsen","scheme":"Visa","last4":"4242","fingerprint":"9F3BAD2E48C6C8579F2F5DC0710B7C11A8ACD5072C3363A72579A6FB227D64BE","bin":"424242","card_type":"Credit","card_category":"Consumer","issuer":"JPMORGAN CHASE BANK NA","issuer_country":"US","product_id":"A","product_type":"Visa Traditional","avs_check":"S","cvv_check":"Y","payouts":true,"fast_funds":"d"},"customer":{"id":"cus_tz76qzbwr44ezdfyzdvrvlwogy","email":"longbob.longsen@example.com","name":"Longbob Longsen"},"processed_on":"2020-09-11T13:58:32Z","reference":"1","processing":{"acquirer_transaction_id":"9819327011","retrieval_reference_number":"861613285622"},"_links":{"self":{"href":"https://api.sandbox.checkout.com/payments/pay_bgv5tmah6fmuzcmcrcro6exe6m"},"actions":{"href":"https://api.sandbox.checkout.com/payments/pay_bgv5tmah6fmuzcmcrcro6exe6m/actions"},"capture":{"href":"https://api.sandbox.checkout.com/payments/pay_bgv5tmah6fmuzcmcrcro6exe6m/captures"},"void":{"href":"https://api.sandbox.checkout.com/payments/pay_bgv5tmah6fmuzcmcrcro6exe6m/voids"}}} ) 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' @@ -815,6 +1183,79 @@ def failed_purchase_response ) end + def failed_purchase_3ds_response + %({ + "id": "pay_awjzhfj776gulbp2nuslj4agbu", + "requested_on": "2019-08-14T18:13:54Z", + "source": { + "id": "src_lot2ch4ygk3ehi4fugxmk7r2di", + "type": "card", + "expiry_month": 12, + "expiry_year": 2020, + "name": "Jane Doe", + "scheme": "Visa", + "last4": "0907", + "fingerprint": "E4048195442B0059D73FD47F6E1961A02CD085B0B34B7703CE4A93750DB5A0A1", + "bin": "457382", + "avs_check": "S", + "cvv_check": "Y" + }, + "amount": 100, + "currency": "USD", + "payment_type": "Regular", + "reference": "Dvy8EMaEphrMWolKsLVHcUqPsyx", + "status": "Declined", + "approved": false, + "3ds": { + "downgraded": false, + "enrolled": "Y", + "authentication_response": "Y", + "cryptogram": "ce49b5c1-5d3c-4864-bd16-2a8c", + "xid": "95202312-f034-48b4-b9b2-54254a2b49fb", + "version": "2.1.0" + }, + "risk": { + "flagged": false + }, + "customer": { + "id": "cus_zt5pspdtkypuvifj7g6roy7p6y", + "name": "Jane Doe" + }, + "billing_descriptor": { + "name": "", + "city": "London" + }, + "payment_ip": "127.0.0.1", + "metadata": { + "Udf5": "ActiveMerchant" + }, + "eci": "05", + "scheme_id": "638284745624527", + "actions": [ + { + "id": "act_tkvif5mf54eerhd3ysuawfcnt4", + "type": "Authorization", + "response_code": "20051", + "response_summary": "Insufficient Funds" + } + ], + "_links": { + "self": { + "href": "https://api.sandbox.checkout.com/payments/pay_tkvif5mf54eerhd3ysuawfcnt4" + }, + "actions": { + "href": "https://api.sandbox.checkout.com/payments/pay_tkvif5mf54eerhd3ysuawfcnt4/actions" + }, + "capture": { + "href": "https://api.sandbox.checkout.com/payments/pay_tkvif5mf54eerhd3ysuawfcnt4/captures" + }, + "void": { + "href": "https://api.sandbox.checkout.com/payments/pay_tkvif5mf54eerhd3ysuawfcnt4/voids" + } + } + }) + end + def successful_authorize_response %( { @@ -1035,6 +1476,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 index baa412181c1..eef5324bd4b 100644 --- a/test/unit/gateways/commerce_hub_test.rb +++ b/test/unit/gateways/commerce_hub_test.rb @@ -8,44 +8,94 @@ def setup @amount = 1204 @credit_card = credit_card('4005550000000019', month: '02', year: '2035', verification_value: '123') - @google_pay = network_tokenization_credit_card('4005550000000019', + @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', + 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', + 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=') + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=' + ) @declined_card = credit_card('4000300011112220', month: '02', year: '2035', verification_value: '123') + @dynamic_descriptors = { + mcc: '1234', + merchant_name: 'Spreedly', + customer_service_number: '555444321', + service_entitlement: '123444555', + dynamic_descriptors_address: { + 'street' => '123 Main Street', + 'houseNumberOrName' => 'Unit B', + 'city' => 'Atlanta', + 'stateOrProvince' => 'GA', + 'postalCode' => '30303', + 'country' => 'US' + } + } @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) @@ -103,6 +153,35 @@ def test_successful_purchase_with_no_supported_source_as_apple_pay assert_success response end + def test_successful_purchase_with_all_dynamic_descriptors + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(@dynamic_descriptors)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['dynamicDescriptors']['mcc'], @dynamic_descriptors[:mcc] + assert_equal request['dynamicDescriptors']['merchantName'], @dynamic_descriptors[:merchant_name] + assert_equal request['dynamicDescriptors']['customerServiceNumber'], @dynamic_descriptors[:customer_service_number] + assert_equal request['dynamicDescriptors']['serviceEntitlement'], @dynamic_descriptors[:service_entitlement] + assert_equal request['dynamicDescriptors']['address'], @dynamic_descriptors[:dynamic_descriptors_address] + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_some_dynamic_descriptors + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(mcc: '1234', customer_service_number: '555444321')) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['dynamicDescriptors']['mcc'], @dynamic_descriptors[:mcc] + assert_nil request['dynamicDescriptors']['merchantName'] + assert_equal request['dynamicDescriptors']['customerServiceNumber'], @dynamic_descriptors[:customer_service_number] + assert_nil request['dynamicDescriptors']['serviceEntitlement'] + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_successful_authorize response = stub_comms do @gateway.authorize(@amount, @credit_card, @options) @@ -125,7 +204,7 @@ def test_failed_purchase_and_authorize response = @gateway.authorize(@amount, @credit_card, @options) assert_failure response - assert_equal 'HOST', response.error_code + assert_equal 'string', response.error_code end def test_successful_parsing_of_billing_and_shipping_addresses @@ -149,11 +228,11 @@ def test_successful_parsing_of_billing_and_shipping_addresses def test_successful_void response = stub_comms do - @gateway.void('authorization123', @options) + @gateway.void('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_equal 'authorization123', request['referenceTransactionDetails']['referenceTransactionId'] + assert_equal 'CHARGES', request['referenceTransactionDetails']['referenceTransactionType'] assert_nil request['transactionDetails']['captureFlag'] end.respond_with(successful_void_and_refund_response) @@ -162,7 +241,7 @@ def test_successful_void def test_successful_refund response = stub_comms do - @gateway.refund(nil, 'authorization123', @options) + @gateway.refund(nil, 'abc123|authorization123', @options) end.check_request do |_endpoint, data, _headers| request = JSON.parse(data) assert_equal request['referenceTransactionDetails']['referenceTransactionId'], 'authorization123' @@ -176,7 +255,7 @@ def test_successful_refund def test_successful_partial_refund response = stub_comms do - @gateway.refund(@amount - 1, 'authorization123', @options) + @gateway.refund(@amount - 1, 'abc123|authorization123', @options) end.check_request do |_endpoint, data, _headers| request = JSON.parse(data) assert_equal request['referenceTransactionDetails']['referenceTransactionId'], 'authorization123' @@ -189,6 +268,74 @@ def test_successful_partial_refund assert_success response end + def test_successful_credit + stub_comms do + @gateway.credit(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_not_nil request['amount'] + assert_equal request['source']['card']['cardData'], @credit_card.number + end.respond_with(successful_credit_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) @@ -206,16 +353,37 @@ def test_successful_store end def test_successful_verify - response = stub_comms do + stub_comms do @gateway.verify(@credit_card, @options) - end.check_request do |_endpoint, data, _headers| + end.check_request do |endpoint, data, _headers| request = JSON.parse(data) - assert_equal request['transactionDetails']['captureFlag'], false - assert_equal request['transactionDetails']['primaryTransactionType'], 'AUTH_ONLY' - assert_equal request['transactionDetails']['accountVerification'], true + assert_match %r{verification}, endpoint + assert_equal request['source']['sourceType'], 'PaymentCard' end.respond_with(successful_authorize_response) + end - assert_success response + 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 @@ -223,6 +391,87 @@ def test_successful_scrub 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 @@ -479,6 +728,107 @@ def successful_void_and_refund_response RESPONSE end + def successful_credit_response + <<~RESPONSE + { + "gatewayResponse": { + "transactionType": "REFUND", + "transactionState": "CAPTURED", + "transactionOrigin": "ECOM", + "transactionProcessingDetails": { + "orderId": "CHG01edceac93c72d31489f14a994f77b5e93", + "transactionTimestamp": "2023-11-22T01:09:26.833753719Z", + "apiTraceId": "4dcb1fc8ea9d4f1084046a77cf250292", + "clientRequestId": "4519030", + "transactionId": "4dcb1fc8ea9d4f1084046a77cf250292" + } + }, + "source": { + "sourceType": "PaymentCard", + "card": { + "nameOnCard": "Joe Bloggs", + "expirationMonth": "02", + "expirationYear": "2035", + "bin": "400555", + "last4": "0019", + "scheme": "VISA" + } + }, + "transactionDetails": { + "captureFlag": true, + "transactionCaptureType": "host", + "processingCode": "200000", + "merchantInvoiceNumber": "593041958876", + "physicalGoodsIndicator": false, + "createToken": true, + "retrievalReferenceNumber": "6a77cf250292" + }, + "transactionInteraction": { + "posEntryMode": "MANUAL", + "posConditionCode": "CARD_NOT_PRESENT_ECOM", + "additionalPosInformation": { + "stan": "009748", + "dataEntrySource": "UNSPECIFIED", + "posFeatures": { + "pinAuthenticationCapability": "UNSPECIFIED", + "terminalEntryCapability": "UNSPECIFIED" + } + }, + "authorizationCharacteristicsIndicator": "N", + "hostPosEntryMode": "010", + "hostPosConditionCode": "59" + }, + "merchantDetails": { + "tokenType": "LTDC", + "terminalId": "10000001", + "merchantId": "100039000301165" + }, + "paymentReceipt": { + "approvedAmount": { + "total": 1.0, + "currency": "USD" + }, + "processorResponseDetails": { + "approvalStatus": "APPROVED", + "approvalCode": "OK7975", + "referenceNumber": "6a77cf250292", + "processor": "FISERV", + "host": "NASHVILLE", + "networkRouted": "VISA", + "networkInternationalId": "0001", + "responseCode": "000", + "responseMessage": "Approved", + "hostResponseCode": "00", + "hostResponseMessage": "APPROVAL", + "responseIndicators": { + "alternateRouteDebitIndicator": false, + "signatureLineIndicator": false, + "signatureDebitRouteIndicator": false + }, + "bankAssociationDetails": { + "associationResponseCode": "V000" + }, + "additionalInfo": [ + { + "name": "HOST_RAW_PROCESSOR_RESPONSE", + "value": "ARAyIAGADoAAAiAAAAAAAAABABEiAQknAJdIAAFZNmE3N2NmMjUwMjkyT0s3OTc1MDAwMTc2MTYxMwGRAEgxNE4wMTMzMjY4MTE5MjEwMTBJViAgICAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxMDAAGDIyQVBQUk9WQUwgICAgICAgIAAGVklDUkggAHRTRFhZMDAzUlNUVEMwMTU2MDExMDAwMDAwMDAwMDBSSTAxNTAwMDAwMDAwMDAwMDAwME5MMDA0VklTQVRZMDAxQ0FSMDA0VjAwMAA1QVJDSTAwM1VOS0NQMDAxP0RQMDAxSFJDMDAyMDBDQjAwMVY=" + } + ] + } + }, + "networkDetails": { + "network": { + "network": "Visa" + }, + "networkResponseCode": "00", + "cardLevelResultCode": "CRH ", + "validationCode": "IV ", + "transactionIdentifier": "013326811921010" + } + } + RESPONSE + end + def successful_store_response <<~RESPONSE { diff --git a/test/unit/gateways/credorax_test.rb b/test/unit/gateways/credorax_test.rb index 76ccef0cab4..625953f57f0 100644 --- a/test/unit/gateways/credorax_test.rb +++ b/test/unit/gateways/credorax_test.rb @@ -42,6 +42,27 @@ 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 @@ -986,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 @@ -1049,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) 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..ba3c6af8af7 --- /dev/null +++ b/test/unit/gateways/cyber_source_rest_test.rb @@ -0,0 +1,818 @@ +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 + ) + @master_card = credit_card('2222420000001113', brand: 'master') + + @visa_network_token = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :network_token + ) + + @mastercard_network_token = network_tokenization_credit_card( + '5555555555554444', + brand: 'master', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :network_token + ) + @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' + } + @discover_card = credit_card('6011111111111117', brand: 'discover') + @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_network_token_visa + stub_comms do + @gateway.authorize(100, @visa_network_token, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '001', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '3', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_equal 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', request['paymentInformation']['tokenizedCard']['cryptogram'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '015', request['processingInformation']['paymentSolution'] + assert_equal 'internet', request['processingInformation']['commerceIndicator'] + end.respond_with(successful_purchase_response) + end + + def test_authorize_network_token_visa_recurring + @options[:stored_credential] = stored_credential(:cardholder, :recurring) + stub_comms do + @gateway.authorize(100, @visa_network_token, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '001', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '3', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_equal 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', request['paymentInformation']['tokenizedCard']['cryptogram'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '015', request['processingInformation']['paymentSolution'] + assert_equal 'recurring', request['processingInformation']['commerceIndicator'] + end.respond_with(successful_purchase_response) + end + + def test_authorize_network_token_visa_installment + @options[:stored_credential] = stored_credential(:cardholder, :installment) + stub_comms do + @gateway.authorize(100, @visa_network_token, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '001', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '3', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_equal 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', request['paymentInformation']['tokenizedCard']['cryptogram'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '015', request['processingInformation']['paymentSolution'] + assert_equal 'install', request['processingInformation']['commerceIndicator'] + end.respond_with(successful_purchase_response) + end + + def test_authorize_network_token_visa_unscheduled + @options[:stored_credential] = stored_credential(:cardholder, :unscheduled) + stub_comms do + @gateway.authorize(100, @visa_network_token, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '001', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '3', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_equal 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', request['paymentInformation']['tokenizedCard']['cryptogram'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '015', request['processingInformation']['paymentSolution'] + assert_equal 'internet', request['processingInformation']['commerceIndicator'] + end.respond_with(successful_purchase_response) + end + + def test_authorize_network_token_mastercard + stub_comms do + @gateway.authorize(100, @mastercard_network_token, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '002', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '3', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_equal 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', request['paymentInformation']['tokenizedCard']['cryptogram'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '014', request['processingInformation']['paymentSolution'] + assert_equal 'internet', request['processingInformation']['commerceIndicator'] + end.respond_with(successful_purchase_response) + 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') + 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', 'storedCredentialUsed') + assert_nil request.dig('processingInformation', 'authorizationOptions', 'initiator', 'merchantInitiatedTransaction', 'originalAuthorizedAmount') + 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_mastercard_purchase_with_3ds2 + @options[:three_d_secure] = { + version: '2.2.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y', + cavv_algorithm: '2' + } + stub_comms do + @gateway.purchase(100, @master_card, @options) + end.check_request do |_endpoint, data, _headers| + json_data = JSON.parse(data) + assert_equal json_data['consumerAuthenticationInformation']['ucafAuthenticationData'], '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=' + assert_equal json_data['consumerAuthenticationInformation']['ucafCollectionIndicator'], '2' + assert_equal json_data['consumerAuthenticationInformation']['cavvAlgorithm'], '2' + assert_equal json_data['consumerAuthenticationInformation']['paSpecificationVersion'], '2.2.0' + assert_equal json_data['consumerAuthenticationInformation']['directoryServerTransactionID'], 'ODUzNTYzOTcwODU5NzY3Qw==' + assert_equal json_data['consumerAuthenticationInformation']['eciRaw'], '05' + assert_equal json_data['consumerAuthenticationInformation']['xid'], '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=' + assert_equal json_data['consumerAuthenticationInformation']['veresEnrolled'], 'true' + assert_equal json_data['consumerAuthenticationInformation']['paresStatus'], 'Y' + end.respond_with(successful_purchase_response) + end + + def test_visa_purchase_with_3ds2 + @options[:three_d_secure] = { + version: '2.2.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y', + cavv_algorithm: '2' + } + 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['consumerAuthenticationInformation']['cavv'], '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=' + assert_equal json_data['consumerAuthenticationInformation']['cavvAlgorithm'], '2' + assert_equal json_data['consumerAuthenticationInformation']['paSpecificationVersion'], '2.2.0' + assert_equal json_data['consumerAuthenticationInformation']['directoryServerTransactionID'], 'ODUzNTYzOTcwODU5NzY3Qw==' + assert_equal json_data['consumerAuthenticationInformation']['eciRaw'], '05' + assert_equal json_data['consumerAuthenticationInformation']['xid'], '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=' + assert_equal json_data['consumerAuthenticationInformation']['veresEnrolled'], 'true' + assert_equal json_data['consumerAuthenticationInformation']['paresStatus'], 'Y' + 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 + + def test_purchase_with_level_2_data + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge({ purchase_order_number: '13829012412' })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '13829012412', request['orderInformation']['invoiceDetails']['purchaseOrderNumber'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_level_3_data + options = { + purchase_order_number: '6789', + discount_amount: '150', + ships_from_postal_code: '90210', + line_items: [ + { + productName: 'Product Name', + kind: 'debit', + quantity: 10, + unitPrice: '9.5000', + totalAmount: '95.00', + taxAmount: '5.00', + discountAmount: '0.00', + productCode: '54321', + commodityCode: '98765' + }, + { + productName: 'Other Product Name', + kind: 'debit', + quantity: 1, + unitPrice: '2.5000', + totalAmount: '90.00', + taxAmount: '2.00', + discountAmount: '1.00', + productCode: '54322', + commodityCode: '98766' + } + ] + } + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(options)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '3', request['processingInformation']['purchaseLevel'] + assert_equal '150', request['orderInformation']['amountDetails']['discountAmount'] + assert_equal '90210', request['orderInformation']['shipping_details']['shipFromPostalCode'] + end.respond_with(successful_purchase_response) + 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 pre_scrubbed_nt + <<-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\":\"ba20ae354e25edd1a5ab27158c0a2955\"},\"paymentInformation\":{\"tokenizedCard\":{\"number\":\"4111111111111111\",\"expirationMonth\":9,\"expirationYear\":2025,\"cryptogram\":\"EHuWW9PiBkWvqE5juRwDzAUFBAk=\",\"type\":\"001\",\"transactionType\":\"3\"}},\"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\"}},\"processingInformation\":{\"commerceIndicator\":\"internet\",\"paymentSolution\":\"015\",\"authorizationOptions\":{}}}" + -> "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/7145981349676498704951/reversals\"},\"self\":{\"method\":\"GET\",\"href\":\"/pts/v2/payments/7145981349676498704951\"},\"capture\":{\"method\":\"POST\",\"href\":\"/pts/v2/payments/7145981349676498704951/captures\"}},\"clientReferenceInformation\":{\"code\":\"ba20ae354e25edd1a5ab27158c0a2955\"},\"id\":\"7145981349676498704951\",\"issuerInformation\":{\"responseRaw\":\"0110322000000E10000200000000000001022105012115353420253130383141564D334B5953323833313030303030000159008000223134573031363135303730333830323039344730363400103232415050524F56414C00065649435243200034544B54523031313132313231323132313231544C3030323636504E30303431313131\"},\"orderInformation\":{\"amountDetails\":{\"authorizedAmount\":\"102.21\",\"currency\":\"USD\"}},\"paymentAccountInformation\":{\"card\":{\"type\":\"001\"}},\"paymentInformation\":{\"tokenizedCard\":{\"requestorId\":\"12121212121\",\"assuranceLevel\":\"66\",\"type\":\"001\"},\"card\":{\"suffix\":\"1111\",\"type\":\"001\"}},\"pointOfSaleInformation\":{\"terminalId\":\"01234567\"},\"processorInformation\":{\"merchantNumber\":\"000123456789012\",\"approvalCode\":\"831000\",\"networkTransactionId\":\"016150703802094\",\"transactionId\":\"016150703802094\",\"responseCode\":\"00\",\"avs\":{\"code\":\"Y\",\"codeRaw\":\"Y\"}},\"reconciliationId\":\"1081AVM3KYS2\",\"status\":\"AUTHORIZED\",\"submitTimeUtc\":\"2024-05-01T21:15:35Z\"}" + PRE + end + + def post_scrubbed_nt + <<-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\":\"ba20ae354e25edd1a5ab27158c0a2955\"},\"paymentInformation\":{\"tokenizedCard\":{\"number\":\"[FILTERED]\",\"expirationMonth\":9,\"expirationYear\":2025,\"cryptogram\":\"[FILTERED]\",\"type\":\"001\",\"transactionType\":\"3\"}},\"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\"}},\"processingInformation\":{\"commerceIndicator\":\"internet\",\"paymentSolution\":\"015\",\"authorizationOptions\":{}}}" + -> "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/7145981349676498704951/reversals\"},\"self\":{\"method\":\"GET\",\"href\":\"/pts/v2/payments/7145981349676498704951\"},\"capture\":{\"method\":\"POST\",\"href\":\"/pts/v2/payments/7145981349676498704951/captures\"}},\"clientReferenceInformation\":{\"code\":\"ba20ae354e25edd1a5ab27158c0a2955\"},\"id\":\"7145981349676498704951\",\"issuerInformation\":{\"responseRaw\":\"0110322000000E10000200000000000001022105012115353420253130383141564D334B5953323833313030303030000159008000223134573031363135303730333830323039344730363400103232415050524F56414C00065649435243200034544B54523031313132313231323132313231544C3030323636504E30303431313131\"},\"orderInformation\":{\"amountDetails\":{\"authorizedAmount\":\"102.21\",\"currency\":\"USD\"}},\"paymentAccountInformation\":{\"card\":{\"type\":\"001\"}},\"paymentInformation\":{\"tokenizedCard\":{\"requestorId\":\"12121212121\",\"assuranceLevel\":\"66\",\"type\":\"001\"},\"card\":{\"suffix\":\"1111\",\"type\":\"001\"}},\"pointOfSaleInformation\":{\"terminalId\":\"01234567\"},\"processorInformation\":{\"merchantNumber\":\"000123456789012\",\"approvalCode\":\"831000\",\"networkTransactionId\":\"016150703802094\",\"transactionId\":\"016150703802094\",\"responseCode\":\"00\",\"avs\":{\"code\":\"Y\",\"codeRaw\":\"Y\"}},\"reconciliationId\":\"1081AVM3KYS2\",\"status\":\"AUTHORIZED\",\"submitTimeUtc\":\"2024-05-01T21:15:35Z\"}" + 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 3e5c97e29b5..4b6fb1c914a 100644 --- a/test/unit/gateways/cyber_source_test.rb +++ b/test/unit/gateways/cyber_source_test.rb @@ -18,6 +18,30 @@ 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) + @network_token_mastercard = network_tokenization_credit_card('5555555555554444', + brand: 'master', + transaction_id: '123', + eci: '05', + source: :network_token, + payment_cryptogram: '111111111100cryptogram') + @amex_network_token = network_tokenization_credit_card('378282246310005', + brand: 'american_express', + 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 = { @@ -34,7 +58,8 @@ def setup national_tax: '5' } ], - currency: 'USD' + currency: 'USD', + reconciliation_id: '181537' } @subscription_options = { @@ -66,8 +91,9 @@ def test_successful_credit_card_purchase def test_successful_purchase_with_other_tax_fields stub_comms do - @gateway.purchase(100, @credit_card, @options.merge(national_tax_indicator: 1, vat_tax_rate: 1.01)) + @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(/MerchantId<\/merchantID>/, data) assert_match(/\s+1.01<\/vatTaxRate>\s+1<\/nationalTaxIndicator>\s+<\/otherTax>/m, data) end.respond_with(successful_purchase_response) end @@ -97,6 +123,22 @@ def test_successful_authorize_with_cc_auth_service_fields end.respond_with(successful_authorization_response) end + def test_successful_authorize_with_cc_auth_service_first_recurring_payment + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(first_recurring_payment: true)) + end.check_request do |_endpoint, data, _headers| + assert_match(/true<\/firstRecurringPayment>/, data) + end.respond_with(successful_authorization_response) + end + + def test_successful_authorize_with_cc_auth_service_aggregator_id + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(aggregator_id: 'ABCDE')) + end.check_request do |_endpoint, data, _headers| + assert_match(/ABCDE<\/aggregatorID>/, data) + end.respond_with(successful_authorization_response) + end + def test_successful_credit_card_purchase_with_elo @gateway.expects(:ssl_post).returns(successful_purchase_response) @@ -134,7 +176,7 @@ def test_purchase_includes_mdd_fields def test_purchase_includes_reconciliation_id stub_comms do - @gateway.purchase(100, @credit_card, order_id: '1', reconciliation_id: '181537') + @gateway.purchase(100, @credit_card, @options.merge(order_id: '1')) end.check_request do |_endpoint, data, _headers| assert_match(/181537<\/reconciliationID>/, data) end.respond_with(successful_purchase_response) @@ -214,6 +256,22 @@ def test_purchase_includes_invoice_header 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) @@ -222,6 +280,22 @@ def test_purchase_includes_tax_management_indicator end.respond_with(successful_purchase_response) end + def test_auth_includes_gratuity_amount + stub_comms do + @gateway.authorize(100, @credit_card, gratuity_amount: '7.50') + end.check_request do |_endpoint, data, _headers| + assert_match(/7.50<\/gratuityAmount>/, data) + end.respond_with(successful_purchase_response) + end + + def test_purchase_includes_gratuity_amount + stub_comms do + @gateway.purchase(100, @credit_card, gratuity_amount: '7.50') + end.check_request do |_endpoint, data, _headers| + assert_match(/7.50<\/gratuityAmount>/, data) + end.respond_with(successful_purchase_response) + end + def test_authorize_includes_issuer_additional_data stub_comms do @gateway.authorize(100, @credit_card, order_id: '1', issuer_additional_data: @issuer_additional_data) @@ -240,7 +314,7 @@ def test_authorize_includes_mdd_fields def test_authorize_includes_reconciliation_id stub_comms do - @gateway.authorize(100, @credit_card, order_id: '1', reconciliation_id: '181537') + @gateway.authorize(100, @credit_card, @options.merge(order_id: '1')) end.check_request do |_endpoint, data, _headers| assert_match(/181537<\/reconciliationID>/, data) end.respond_with(successful_authorization_response) @@ -280,6 +354,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') @@ -345,6 +435,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 @@ -377,9 +479,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 @@ -390,9 +502,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| @@ -401,9 +511,50 @@ 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 @@ -442,6 +593,24 @@ def test_successful_auth_request assert response.test? end + def test_successful_reconciliation_id_2 + @gateway.stubs(:ssl_post).returns(successful_purchase_and_capture_response) + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_equal response.params['reconciliationID'], 'abcdf' + assert_equal response.params['reconciliationID2'], '31159291T3XM2B13' + assert response.success? + assert response.test? + end + + def test_successful_authorization_without_reconciliation_id_2 + @gateway.stubs(:ssl_post).returns(successful_authorization_response) + assert response = @gateway.authorize(@amount, @credit_card, @options) + assert_equal response.params['reconciliationID2'], nil + assert_equal response.params['reconciliationID'], '23439130C40VZ2FB' + assert response.success? + assert response.test? + end + def test_successful_auth_with_elo_request @gateway.stubs(:ssl_post).returns(successful_authorization_response) assert response = @gateway.authorize(@amount, @elo_credit_card, @options) @@ -814,31 +983,21 @@ 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(111111111100cryptogram), body + assert_match %r(internet), body + assert_match %r(3), 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 @@ -848,34 +1007,76 @@ def test_successful_purchase_with_network_tokenization_for_visa end 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 + @gateway.expects(:ssl_post).with do |_host, body| + assert_xml_valid_to_xsd(body) + assert_match %r(111111111100cryptogram), body + assert_match %r(internet), body + assert_match %r(3), body + assert_match %r(trid_123), body + assert_match %r(014), 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', + source: :network_token + ) - assert response = @gateway.authorize(@amount, credit_card, @options) + assert response = @gateway.authorize(@amount, credit_card, @options.merge!(trid: 'trid_123')) + assert_success response + end + + def test_successful_purchase_network_tokenization_mastercard + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_xml_valid_to_xsd(request_body) + assert_match %r'111111111100cryptogram', request_body + assert_match %r'internet', request_body + assert_match %r'014', request_body + assert_not_match %r'111111111100cryptogram', request_body + true + end.returns(successful_purchase_response) + + assert response = @gateway.purchase(@amount, @network_token_mastercard, @options) + assert_success response + end + + def test_successful_purchase_network_tokenization_amex + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_xml_valid_to_xsd(request_body) + assert_match %r'111111111100cryptogram', request_body + assert_match %r'internet', request_body + assert_not_match %r'014', request_body + assert_not_match %r'015', request_body + true + end.returns(successful_purchase_response) + + assert response = @gateway.purchase(@amount, @amex_network_token, @options) assert_success response end 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'MTExMTExMTExMTAwY3J5cHRvZ3JhbQ==\n', request_body + assert_match %r'internet', request_body + assert_not_match %r'014', request_body + assert_not_match %r'015', request_body + assert_match %r'181537', 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'), + source: :network_token + ) assert response = @gateway.authorize(@amount, credit_card, @options) assert_success response @@ -1031,6 +1232,214 @@ def test_nonfractional_currency_handling assert_success response end + # CITs/MITs For Network Tokens + + def test_cit_unscheduled_network_token + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: 'unscheduled', + initial_transaction: true + } + response = stub_comms do + @gateway.authorize(@amount, @network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\181537/, data) + assert_match(/\111111111100cryptogram/, data) + assert_match(/\015/, data) + assert_match(/\3/, data) + assert_match(/\true/, data) + assert_match(/\internet/, data) + assert_not_match(/\/, data) + assert_not_match(/\true/, data) + assert_not_match(/\016150703802094/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_mit_unscheduled_network_token + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'unscheduled', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + response = stub_comms do + @gateway.authorize(@amount, @network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\181537/, data) + assert_match(/\111111111100cryptogram/, data) + assert_match(/\015/, data) + assert_match(/\3/, data) + assert_not_match(/\true/, data) + assert_match(/\true/, data) + assert_match(/\true/, data) + assert_match(/\016150703802094/, data) + assert_match(/\internet/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_subsequent_cit_unscheduled_network_token + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: 'unscheduled', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + response = stub_comms do + @gateway.authorize(@amount, @network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\181537/, data) + assert_match(/\111111111100cryptogram/, data) + assert_match(/\015/, data) + assert_match(/\3/, data) + assert_not_match(/\true/, data) + assert_match(/\true/, data) + assert_not_match(/\true/, data) + assert_not_match(/\016150703802094/, data) + assert_match(/\internet/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_cit_installment_network_token + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: 'installment', + initial_transaction: true + } + response = stub_comms do + @gateway.authorize(@amount, @network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\181537/, data) + assert_match(/\111111111100cryptogram/, data) + assert_match(/\015/, data) + assert_match(/\3/, data) + assert_match(/\true/, data) + assert_match(/\internet/, data) + assert_not_match(/\/, data) + assert_not_match(/\true/, data) + assert_not_match(/\016150703802094/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_mit_installment_network_token + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'installment', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + response = stub_comms do + @gateway.authorize(@amount, @network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\181537/, data) + assert_match(/\111111111100cryptogram/, data) + assert_match(/\015/, data) + assert_match(/\3/, data) + assert_not_match(/\true/, data) + assert_not_match(/\true/, data) + assert_match(/\true/, data) + assert_match(/\016150703802094/, data) + assert_match(/\internet/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_subsequent_cit_installment_network_token + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: 'installment', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + response = stub_comms do + @gateway.authorize(@amount, @network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\181537/, data) + assert_match(/\111111111100cryptogram/, data) + assert_match(/\015/, data) + assert_match(/\3/, data) + assert_not_match(/\/, data) + assert_match(/\true/, data) + assert_not_match(/\true/, data) + assert_not_match(/\016150703802094/, data) + assert_match(/\internet/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_cit_recurring_network_token + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: 'recurring', + initial_transaction: true + } + response = stub_comms do + @gateway.authorize(@amount, @network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\181537/, data) + assert_match(/\111111111100cryptogram/, data) + assert_match(/\015/, data) + assert_match(/\3/, data) + assert_match(/\true/, data) + assert_match(/\internet/, data) + assert_not_match(/\/, data) + assert_not_match(/\true/, data) + assert_not_match(/\016150703802094/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_mit_recurring_network_token + @options[:stored_credential] = { + initiator: 'merchant', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + response = stub_comms do + @gateway.authorize(@amount, @network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\181537/, data) + assert_match(/\111111111100cryptogram/, data) + assert_match(/\015/, data) + assert_match(/\3/, data) + assert_not_match(/\true/, data) + assert_not_match(/\true/, data) + assert_match(/\true/, data) + assert_match(/\016150703802094/, data) + assert_match(/\internet/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + def test_subsequent_cit_recurring_network_token + @options[:stored_credential] = { + initiator: 'cardholder', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: '016150703802094' + } + response = stub_comms do + @gateway.authorize(@amount, @network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\181537/, data) + assert_match(/\111111111100cryptogram/, data) + assert_match(/\015/, data) + assert_match(/\3/, data) + assert_not_match(/\/, data) + assert_match(/\true/, data) + assert_not_match(/\true/, data) + assert_not_match(/\016150703802094/, data) + assert_match(/\internet/, data) + end.respond_with(successful_authorization_response) + assert response.success? + end + + # CITs/MITs for Network Tokens + def test_malformed_xml_handling @gateway.expects(:ssl_post).returns(malformed_xml_response) @@ -1302,10 +1711,90 @@ 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 + def test_scrub_network_token + assert_equal @gateway.scrub(pre_scrubbed_network_token), post_scrubbed_network_token + end + def test_supports_scrubbing? assert @gateway.supports_scrubbing? end @@ -1408,11 +1897,41 @@ 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) + credit_card = network_tokenization_credit_card('4111111111111111', brand: 'american_express', payment_cryptogram: long_cryptogram) stub_comms do @gateway.authorize(@amount, credit_card, @options) @@ -1426,9 +1945,7 @@ def test_able_to_properly_handle_40bytes_cryptogram end def test_able_to_properly_handle_20bytes_cryptogram - credit_card = network_tokenization_credit_card('4111111111111111', - brand: 'american_express', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') + credit_card = network_tokenization_credit_card('4111111111111111', brand: 'american_express', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') stub_comms do @gateway.authorize(@amount, credit_card, @options) @@ -1439,27 +1956,37 @@ def test_able_to_properly_handle_20bytes_cryptogram 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=') + def test_returns_error_on_network_token_with_an_underlying_discover_card + credit_card = network_tokenization_credit_card('4111111111111111', brand: 'discover', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', source: :network_token) + response = @gateway.authorize(100, credit_card, @options) - @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 + assert_equal response.message, 'Discover is not supported by NetworkToken at CyberSource, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html' 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=') + def test_returns_error_on_apple_pay_with_an_underlying_discover_card + credit_card = network_tokenization_credit_card('4111111111111111', brand: 'discover', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', source: :apple_pay) + response = @gateway.purchase(100, credit_card, @options) - @gateway.authorize(100, credit_card, @options) - end + assert_equal response.message, 'Discover is not supported by ApplePay at CyberSource, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html' + end + + def test_returns_error_on_google_pay_with_an_underlying_discover_card + credit_card = network_tokenization_credit_card('4111111111111111', brand: 'discover', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', source: :google_pay) + response = @gateway.store(credit_card, @options) - 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 + assert_equal response.message, 'Discover is not supported by GooglePay at CyberSource, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html' + 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 @@ -1521,6 +2048,54 @@ def pre_scrubbed PRE_SCRUBBED end + def pre_scrubbed_network_token + <<-PRE_SCRUBBED + opening connection to ics2wstest.ic3.com:443... + opened + starting SSL for ics2wstest.ic3.com:443... + SSL established + <- "POST /commerce/1.x/transactionProcessor HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\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: ics2wstest.ic3.com\r\nContent-Length: 2459\r\n\r\n" + <- "\n\n \n \n \n l\n p\n \n \n \n \n \n l\n 1000\n Ruby Active Merchant\n 1.135.0\n arm64-darwin22\n\n Longbob\n Longsen\n Unspecified\n Unspecified\n NC\n 00000\n US\n null@cybersource.com\n 127.0.0.1\n\n\n Longbob\n Longsen\n \n \n \n \n null@cybersource.com\n\n\n 1.00\n 2\n default\n Giant Walrus\n WA323232323232323\n 10\n 5\n\n\n USD\n 1.00\n\n\n 5555555555554444\n 09\n 2025\n 123\n 002\n\n\n 111111111100cryptogram\n internet\n\n\n\n\n trid_123\n 3\n\n014\n \n \n\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: Apache-Coyote/1.1\r\n" + -> "X-OPNET-Transaction-Trace: pid=18901,requestid=08985faa-d84a-4200-af8a-1d0a4d50f391\r\n" + -> "Set-Cookie: _op_aixPageId=a_233cede6-657e-481e-977d-a4a886dafd37; Path=/\r\n" + -> "Content-Type: text/xml\r\n" + -> "Content-Length: 1572\r\n" + -> "Date: Fri, 05 Jun 2015 13:01:57 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 1572 bytes... + -> "\n\n2015-06-05T13:01:57.974Z734dda9bb6446f2f2638ab7faf34682f4335093172165000001515ACCEPT100Ahj//wSR1gMBn41YRu/WIkGLlo3asGzCbBky4VOjHT9/xXHSYBT9/xXHSbSA+RQkhk0ky3SA3+mwMCcjrAYDPxqwjd+sKWXLUSD1001.00888888XI12015-06-05T13:01:57Z10019475060MAIKBSQG1002015-06-05T13:01:57Z1.0019475060MAIKBSQG" + read 1572 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed_network_token + <<-PRE_SCRUBBED + opening connection to ics2wstest.ic3.com:443... + opened + starting SSL for ics2wstest.ic3.com:443... + SSL established + <- "POST /commerce/1.x/transactionProcessor HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\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: ics2wstest.ic3.com\r\nContent-Length: 2459\r\n\r\n" + <- "\n\n \n \n \n l\n [FILTERED]\n \n \n \n \n \n l\n 1000\n Ruby Active Merchant\n 1.135.0\n arm64-darwin22\n\n Longbob\n Longsen\n Unspecified\n Unspecified\n NC\n 00000\n US\n null@cybersource.com\n 127.0.0.1\n\n\n Longbob\n Longsen\n \n \n \n \n null@cybersource.com\n\n\n 1.00\n 2\n default\n Giant Walrus\n WA323232323232323\n 10\n 5\n\n\n USD\n 1.00\n\n\n [FILTERED]\n 09\n 2025\n [FILTERED]\n 002\n\n\n [FILTERED]\n internet\n\n\n\n\n [FILTERED]\n 3\n\n014\n \n \n\n" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: Apache-Coyote/1.1\r\n" + -> "X-OPNET-Transaction-Trace: pid=18901,requestid=08985faa-d84a-4200-af8a-1d0a4d50f391\r\n" + -> "Set-Cookie: _op_aixPageId=a_233cede6-657e-481e-977d-a4a886dafd37; Path=/\r\n" + -> "Content-Type: text/xml\r\n" + -> "Content-Length: 1572\r\n" + -> "Date: Fri, 05 Jun 2015 13:01:57 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 1572 bytes... + -> "\n\n2015-06-05T13:01:57.974Z734dda9bb6446f2f2638ab7faf34682f4335093172165000001515ACCEPT100Ahj//wSR1gMBn41YRu/WIkGLlo3asGzCbBky4VOjHT9/xXHSYBT9/xXHSbSA+RQkhk0ky3SA3+mwMCcjrAYDPxqwjd+sKWXLUSD1001.00888888XI12015-06-05T13:01:57Z10019475060MAIKBSQG1002015-06-05T13:01:57Z1.0019475060MAIKBSQG" + read 1572 bytes + Conn close + PRE_SCRUBBED + end + def post_scrubbed <<-POST_SCRUBBED opening connection to ics2wstest.ic3.com:443... @@ -1553,6 +2128,14 @@ def successful_purchase_response XML end + def successful_purchase_and_capture_response + <<~XML + + + 2008-01-15T21:42:03.343Zb0a6cf9aa07f1a8495f89c364bbd6a9a2004333231260008401927ACCEPT100Afvvj7Ke2Fmsbq0wHFE2sM6R4GAptYZ0jwPSA+R9PhkyhFTb0KRjoE4+ynthZrG6tMBwjAtTUSD1001.00abcdf123456YYMM2008-01-15T21:42:03Z00U1002007-07-17T17:15:32Z1.0031159291T3XM2B13 + XML + end + def successful_authorization_response <<~XML @@ -1776,6 +2359,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 33ea7361c74..a1bf4c354ff 100644 --- a/test/unit/gateways/d_local_test.rb +++ b/test/unit/gateways/d_local_test.rb @@ -36,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) @@ -57,8 +65,7 @@ def test_purchase_with_installments end def test_purchase_with_network_tokens - credit_card = network_tokenization_credit_card('4242424242424242', - payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + 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| @@ -68,9 +75,8 @@ def test_purchase_with_network_tokens 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=') + options = @options.merge!(stored_credential: stored_credential(:merchant, :recurring, network_transaction_id: '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| @@ -81,9 +87,8 @@ def test_purchase_with_network_tokens_and_store_credential_type_subscription 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=') + options = @options.merge!(stored_credential: stored_credential(:merchant, :unscheduled, network_transaction_id: '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| @@ -95,8 +100,7 @@ def test_purchase_with_network_tokens_and_store_credential_type_uneschedule 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=') + 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| @@ -107,9 +111,8 @@ def test_purchase_with_network_tokens_and_store_credential_usage_first 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=') + options = @options.merge!(stored_credential: stored_credential(:cardholder, :unscheduled, network_transaction_id: '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| @@ -121,9 +124,8 @@ def test_purchase_with_network_tokens_and_store_credential_type_card_on_file_and 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=') + options = @options.merge!(stored_credential: stored_credential(:cardholder, :recurring, network_transaction_id: '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| @@ -133,6 +135,22 @@ def test_purchase_with_network_tokens_and_store_credential_usage end.respond_with(successful_purchase_response) end + def test_purchase_with_ntid_and_store_credential_for_mit + options = @options.merge!(stored_credential: stored_credential(:merchant, :recurring, network_transaction_id: 'abc123')) + credit_card = network_tokenization_credit_card('4242424242424242', payment_cryptogram: 'BwABB4JRdgAAAAAAiFF2AAAAAAA=') + response = stub_comms do + @gateway.purchase(@amount, credit_card, options) + end.check_request do |_endpoint, data, _headers| + response = JSON.parse(data) + assert_equal 'BwABB4JRdgAAAAAAiFF2AAAAAAA=', response['card']['cryptogram'] + assert_equal '4242424242424242', response['card']['network_token'] + assert_equal 'USED', response['card']['stored_credential_usage'] + assert_equal 'abc123', response['card']['network_payment_reference'] + end.respond_with(successful_purchase_with_network_tx_reference_response) + + assert_equal 'MCC000000355', response.network_transaction_id + end + def test_successful_purchase_with_additional_data additional_data = { 'submerchant' => { 'name' => 'socks' } } @@ -566,4 +584,8 @@ def successful_void_response def failed_void_response '{"code":5002,"message":"Invalid transaction status"}' end + + def successful_purchase_with_network_tx_reference_response + '{"id":"D-4-80ca7fbd-67ad-444a-aa88-791ca4a0c2b2","amount":120.00,"currency":"BRL","country":"BR","payment_method_id":"VD","payment_method_flow":"DIRECT","payer":{"name":"ThiagoGabriel","email":"thiago@example.com","document":"53033315550","user_reference":"12345","address":{"state":"RiodeJaneiro","city":"VoltaRedonda","zip_code":"27275-595","street":"ServidaoB-1","number":"1106"}},"card":{"holder_name":"ThiagoGabriel","expiration_month":10,"expiration_year":2040,"brand":"VI","network_tx_reference":"MCC000000355"},"order_id":"657434343","status":"PAID","notification_url":"http://merchant.com/notifications"}' + end end diff --git a/test/unit/gateways/datatrans_test.rb b/test/unit/gateways/datatrans_test.rb new file mode 100644 index 00000000000..532cea6b645 --- /dev/null +++ b/test/unit/gateways/datatrans_test.rb @@ -0,0 +1,374 @@ +require 'test_helper' + +class DatatransTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = DatatransGateway.new(fixtures(:datatrans)) + @credit_card = credit_card + @amount = 100 + + @options = { + order_id: SecureRandom.random_number(1000000000), + email: 'john.smith@test.com' + } + + @three_d_secure_options = @options.merge({ + three_d_secure: { + eci: '05', + cavv: '3q2+78r+ur7erb7vyv66vv8=', + cavv_algorithm: '1', + xid: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'Y', + authentication_response_status: 'Y', + directory_response_status: 'Y', + version: '2', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC' + } + }) + + @transaction_reference = '240214093712238757|093712' + + @billing_address = address + + @nt_credit_card = network_tokenization_credit_card( + '4111111111111111', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + eci: '07', + source: :network_token, + verification_value: '737', + brand: 'visa' + ) + + @apple_pay_card = network_tokenization_credit_card( + '4900000000000094', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '06', + year: '2025', + source: 'apple_pay', + verification_value: 569 + ) + end + + def test_authorize_with_credit_card + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + common_assertions_authorize_purchase(endpoint, parsed_data) + assert_equal(@credit_card.number, parsed_data['card']['number']) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_authorize_with_credit_card_and_billing_address + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options.merge({ billing_address: @billing_address })) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + common_assertions_authorize_purchase(endpoint, parsed_data) + assert_equal(@credit_card.number, parsed_data['card']['number']) + + billing = parsed_data['billing'] + assert_equal('Jim Smith', billing['name']) + assert_equal(@billing_address[:address1], billing['street']) + assert_match(@billing_address[:address2], billing['street2']) + assert_match(@billing_address[:city], billing['city']) + assert_match(@billing_address[:country], billing['country']) + assert_match(@billing_address[:phone], billing['phoneNumber']) + assert_match(@billing_address[:zip], billing['zipCode']) + assert_match(@options[:email], billing['email']) + end.respond_with(successful_authorize_response) + + assert_success response + end + + def test_purchase_with_credit_card + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + common_assertions_authorize_purchase(endpoint, parsed_data) + assert_equal(@credit_card.number, parsed_data['card']['number']) + + assert_equal(true, parsed_data['autoSettle']) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_purchase_with_network_token + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @nt_credit_card, @options) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + common_assertions_authorize_purchase(endpoint, parsed_data) + assert_match('"autoSettle":true', data) + + assert_equal(@nt_credit_card.number, parsed_data['card']['token']) + assert_equal('NETWORK_TOKEN', parsed_data['card']['type']) + assert_equal('VISA', parsed_data['card']['tokenType']) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_authorize_with_apple_pay + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @apple_pay_card, @options) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + common_assertions_authorize_purchase(endpoint, parsed_data) + assert_match('"autoSettle":true', data) + + assert_equal(@apple_pay_card.number, parsed_data['card']['token']) + assert_equal('DEVICE_TOKEN', parsed_data['card']['type']) + assert_equal('APPLE_PAY', parsed_data['card']['tokenType']) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_purchase_with_3ds + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @three_d_secure_options) + end.check_request do |_action, endpoint, data, _headers| + three_d_secure = @three_d_secure_options[:three_d_secure] + parsed_data = JSON.parse(data) + common_assertions_authorize_purchase(endpoint, parsed_data) + assert_include(parsed_data, 'card') + assert_include(parsed_data['card'], '3D') + + parsed_3d = parsed_data['card']['3D'] + + assert_equal('05', parsed_3d['eci']) + assert_equal(three_d_secure[:xid], parsed_3d['xid']) + assert_equal(three_d_secure[:ds_transaction_id], parsed_3d['threeDSTransactionId']) + assert_equal(three_d_secure[:cavv], parsed_3d['cavv']) + assert_equal('2', parsed_3d['threeDSVersion']) + assert_equal(three_d_secure[:cavv_algorithm], parsed_3d['cavvAlgorithm']) + assert_equal(three_d_secure[:authentication_response_status], parsed_3d['authenticationResponse']) + assert_equal(three_d_secure[:directory_response_status], parsed_3d['directoryResponse']) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_capture + response = stub_comms(@gateway, :ssl_request) do + @gateway.capture(@amount, @transaction_reference, @options) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + assert_match('240214093712238757/settle', endpoint) + assert_equal(@options[:order_id], parsed_data['refno']) + assert_equal('CHF', parsed_data['currency']) + assert_equal('100', parsed_data['amount']) + end.respond_with(successful_capture_response) + + assert_success response + end + + def test_refund + response = stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, @transaction_reference, @options) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + assert_match('240214093712238757/credit', endpoint) + assert_equal(@options[:order_id], parsed_data['refno']) + assert_equal('CHF', parsed_data['currency']) + assert_equal('100', parsed_data['amount']) + end.respond_with(successful_refund_response) + + assert_success response + end + + def test_voids + response = stub_comms(@gateway, :ssl_request) do + @gateway.void(@transaction_reference, @options) + end.check_request do |_action, endpoint, data, _headers| + assert_match('240214093712238757/cancel', endpoint) + assert_equal data, '{}' + end.respond_with(successful_void_response) + + assert_success response + end + + def test_required_merchant_id_and_password + error = assert_raises ArgumentError do + DatatransGateway.new + end + + assert_equal 'Missing required parameter: merchant_id', error.message + end + + def test_supported_card_types + assert_equal DatatransGateway.supported_cardtypes, %i[master visa american_express unionpay diners_club discover jcb maestro dankort] + end + + def test_supported_countries + assert_equal DatatransGateway.supported_countries, %w[CH GR US] + end + + def test_support_scrubbing_flag_enabled + assert @gateway.supports_scrubbing? + end + + def test_detecting_successfull_response_from_capture + assert @gateway.send :success_from, 'settle', { 'response_code' => 204 } + end + + def test_detecting_successfull_response_from_purchase + assert @gateway.send :success_from, 'authorize', { 'transactionId' => '2124504', 'acquirerAuthorizationCode' => '12345t' } + end + + def test_detecting_successfull_response_from_authorize + assert @gateway.send :success_from, 'authorize', { 'transactionId' => '2124504', 'acquirerAuthorizationCode' => '12345t' } + end + + def test_detecting_successfull_response_from_refund + assert @gateway.send :success_from, 'credit', { 'transactionId' => '2124504', 'acquirerAuthorizationCode' => '12345t' } + end + + def test_detecting_successfull_response_from_void + assert @gateway.send :success_from, 'cancel', { 'response_code' => 204 } + end + + def test_get_response_message_from_messages_key + message = @gateway.send :message_from, false, { 'error' => { 'message' => 'hello' } } + assert_equal 'hello', message + + message = @gateway.send :message_from, true, {} + assert_equal nil, message + end + + def test_get_response_message_from_message_user + message = @gateway.send :message_from, 'order', { other_key: 'something_else' } + assert_nil message + end + + def test_url_generation_from_action + action = 'test' + assert_equal "#{@gateway.test_url}#{action}", @gateway.send(:url, action) + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal post_scrubbed, @gateway.scrub(pre_scrubbed) + end + + def test_authorization_from + assert_equal '1234|9248', @gateway.send(:authorization_from, { 'transactionId' => '1234', 'acquirerAuthorizationCode' => '9248' }) + assert_equal '1234|', @gateway.send(:authorization_from, { 'transactionId' => '1234' }) + assert_equal '|9248', @gateway.send(:authorization_from, { 'acquirerAuthorizationCode' => '9248' }) + assert_equal nil, @gateway.send(:authorization_from, {}) + end + + def test_parse + assert_equal @gateway.send(:parse, '{"response_code":204}'), { 'response_code' => 204 } + assert_equal @gateway.send(:parse, '{"transactionId":"240418170233899207","acquirerAuthorizationCode":"170233"}'), { 'transactionId' => '240418170233899207', 'acquirerAuthorizationCode' => '170233' } + + assert_equal @gateway.send(:parse, + '{"transactionId":"240418170233899207",acquirerAuthorizationCode":"170233"}'), + { 'successful' => false, + 'response' => {}, + 'errors' => + ['Invalid JSON response received from Datatrans. Please contact them for support if you continue to receive this message. (The raw response returned by the API was "{\\"transactionId\\":\\"240418170233899207\\",acquirerAuthorizationCode\\":\\"170233\\"}")'] } + end + + private + + def successful_authorize_response + '{ + "transactionId":"240214093712238757", + "acquirerAuthorizationCode":"093712" + }' + end + + def successful_capture_response + '{"response_code": 204}' + end + + def common_assertions_authorize_purchase(endpoint, parsed_data) + assert_match('authorize', endpoint) + assert_equal(@options[:order_id], parsed_data['refno']) + assert_equal('CHF', parsed_data['currency']) + assert_equal('100', parsed_data['amount']) + end + + alias successful_purchase_response successful_authorize_response + alias successful_refund_response successful_authorize_response + alias successful_void_response successful_capture_response + + def pre_scrubbed + <<~PRE_SCRUBBED + "opening connection to api.sandbox.datatrans.com:443...\n + opened\n + starting SSL for api.sandbox.datatrans.com:443...\n + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384\n + <- \"POST /v1/transactions/authorize HTTP/1.1\\r\\n + Content-Type: application/json; charset=UTF-8\\r\\n + Authorization: Basic someDataAuth\\r\\n + Connection: close\\r\\n + Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\\n + Accept: */*\\r\\n + User-Agent: Ruby\\r\\n + Host: api.sandbox.datatrans.com\\r\\n + Content-Length: 157\\r\\n\\r\\n\"\n + <- \"{\\\"card\\\":{\\\"number\\\":\\\"4242424242424242\\\",\\\"cvv\\\":\\\"123\\\",\\\"expiryMonth\\\":\\\"06\\\",\\\"expiryYear\\\":\\\"25\\\"},\\\"refno\\\":\\\"683040814\\\",\\\"currency\\\":\\\"CHF\\\",\\\"amount\\\":\\\"756\\\",\\\"autoSettle\\\":true}\"\n + -> \"HTTP/1.1 200 \\r\\n\"\n + -> \"Server: nginx\\r\\n\"\n + -> \"Date: Thu, 18 Apr 2024 15:02:34 GMT\\r\\n\"\n + -> \"Content-Type: application/json\\r\\n\"\n + -> \"Content-Length: 86\\r\\n\"\n + -> \"Connection: close\\r\\n\"\n + -> \"Strict-Transport-Security: max-age=31536000; includeSubdomains\\r\\n\"\n + -> \"P3P: CP=\\\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\\\"\\r\\n\"\n + -> \"X-XSS-Protection: 1; mode=block\\r\\n\"\n + -> \"Correlation-Id: abda35b0-44ac-4a42-8811-941488acc21b\\r\\n\"\n + -> \"\\r\\n\"\nreading 86 bytes...\n + -> \"{\\n + \\\"transactionId\\\" : \\\"240418170233899207\\\",\\n + \\\"acquirerAuthorizationCode\\\" : \\\"170233\\\"\\n + }\"\n + read 86 bytes\n + Conn close\n" + PRE_SCRUBBED + end + + def post_scrubbed + <<~POST_SCRUBBED + "opening connection to api.sandbox.datatrans.com:443...\n + opened\n + starting SSL for api.sandbox.datatrans.com:443...\n + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384\n + <- \"POST /v1/transactions/authorize HTTP/1.1\\r\\n + Content-Type: application/json; charset=UTF-8\\r\\n + Authorization: Basic [FILTERED]\\r\\n + Connection: close\\r\\n + Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\\n + Accept: */*\\r\\n + User-Agent: Ruby\\r\\n + Host: api.sandbox.datatrans.com\\r\\n + Content-Length: 157\\r\\n\\r\\n\"\n + <- \"{\\\"card\\\":{\\\"number\\\":\\\"[FILTERED]\\\",\\\"cvv\\\":\\\"[FILTERED]\\\",\\\"expiryMonth\\\":\\\"06\\\",\\\"expiryYear\\\":\\\"25\\\"},\\\"refno\\\":\\\"683040814\\\",\\\"currency\\\":\\\"CHF\\\",\\\"amount\\\":\\\"756\\\",\\\"autoSettle\\\":true}\"\n + -> \"HTTP/1.1 200 \\r\\n\"\n + -> \"Server: nginx\\r\\n\"\n + -> \"Date: Thu, 18 Apr 2024 15:02:34 GMT\\r\\n\"\n + -> \"Content-Type: application/json\\r\\n\"\n + -> \"Content-Length: 86\\r\\n\"\n + -> \"Connection: close\\r\\n\"\n + -> \"Strict-Transport-Security: max-age=31536000; includeSubdomains\\r\\n\"\n + -> \"P3P: CP=\\\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\\\"\\r\\n\"\n + -> \"X-XSS-Protection: 1; mode=block\\r\\n\"\n + -> \"Correlation-Id: abda35b0-44ac-4a42-8811-941488acc21b\\r\\n\"\n + -> \"\\r\\n\"\nreading 86 bytes...\n + -> \"{\\n + \\\"transactionId\\\" : \\\"240418170233899207\\\",\\n + \\\"acquirerAuthorizationCode\\\" : \\\"170233\\\"\\n + }\"\n + read 86 bytes\n + Conn close\n" + POST_SCRUBBED + end +end diff --git a/test/unit/gateways/decidir_test.rb b/test/unit/gateways/decidir_test.rb index 300d7d40974..be2c78a3f96 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 @@ -159,6 +166,19 @@ def test_successful_purchase_with_sub_payments assert_success response end + def test_successful_purchase_with_customer_object + options = @options.merge(customer_id: 'John', customer_email: 'decidir@decidir.com') + + response = stub_comms(@gateway_for_purchase, :ssl_request) do + @gateway_for_purchase.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert data =~ /"email":"decidir@decidir.com"/ + assert data =~ /"id":"John"/ + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_failed_purchase @gateway_for_purchase.expects(:ssl_request).returns(failed_purchase_response) @@ -378,6 +398,22 @@ def test_successful_inquire_with_authorization 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 @@ -549,6 +585,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"} 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 06e3d4b6db0..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) @@ -136,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 @@ -152,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 @@ -172,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) @@ -223,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/elavon_test.rb b/test/unit/gateways/elavon_test.rb index da97607f95a..49f8ca8c207 100644 --- a/test/unit/gateways/elavon_test.rb +++ b/test/unit/gateways/elavon_test.rb @@ -32,6 +32,16 @@ def setup billing_address: address, description: 'Store Purchase' } + + @google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :google_pay, + payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}" + }) + + @apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :apple_pay, + payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}" + }) end def test_successful_purchase @@ -145,6 +155,22 @@ def test_successful_purchase_with_unscheduled end.respond_with(successful_purchase_response) end + def test_successful_purchase_with_apple_pay + stub_comms do + @gateway.purchase(@amount, @apple_pay, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/%7B %27version%27%3A %27EC_v1%27%2C %27data%27%3A %27QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%27%7D<\/ssl_applepay_web>/, data) + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_google_pay + stub_comms do + @gateway.purchase(@amount, @google_pay, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/%7B %27version%27%3A %27EC_v1%27%2C %27data%27%3A %27QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%27%7D<\/ssl_google_pay>/, data) + end.respond_with(successful_purchase_response) + end + def test_sends_ssl_add_token_field response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(add_recurring_token: 'Y')) diff --git a/test/unit/gateways/element_test.rb b/test/unit/gateways/element_test.rb index ece96d068e5..694af43d9a9 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) @@ -154,6 +166,50 @@ def test_successful_purchase_with_card_present_code assert_success response end + def test_successful_purchase_with_lodging_and_other_fields + lodging_options = { + order_id: '2', + billing_address: address.merge(zip: '87654'), + description: 'Store Purchase', + duplicate_override_flag: 'true', + lodging: { + agreement_number: 182726718192, + check_in_date: 20250910, + check_out_date: 20250915, + room_amount: 1000, + room_tax: 0, + no_show_indicator: 0, + duration: 5, + customer_name: 'francois dubois', + client_code: 'Default', + extra_charges_detail: '01', + extra_charges_amounts: 'Default', + prestigious_property_code: 'DollarLimit500', + special_program_code: 'Sale', + charge_type: 'Restaurant' + } + } + response = stub_comms do + @gateway.purchase(@amount, @credit_card, lodging_options) + end.check_request do |_endpoint, data, _headers| + assert_match '182726718192', data + assert_match '20250910', data + assert_match '20250915', data + assert_match '1000', data + assert_match '0', data + assert_match '0', data + assert_match '5', data + assert_match 'francois dubois', data + assert_match 'Default', data + assert_match '01', data + assert_match 'Default', data + assert_match 'DollarLimit500', data + assert_match 'Sale', data + assert_match 'Restaurant', data + end.respond_with(successful_purchase_response) + assert_success response + end + def test_successful_purchase_with_payment_type response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(payment_type: 'NotUsed')) 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/fat_zebra_test.rb b/test/unit/gateways/fat_zebra_test.rb index 4e3e8a2b00f..3caf94852dc 100644 --- a/test/unit/gateways/fat_zebra_test.rb +++ b/test/unit/gateways/fat_zebra_test.rb @@ -18,6 +18,15 @@ def setup description: 'Store Purchase', extra: { card_on_file: false } } + + @three_ds_secure = { + version: '2.2.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + xid: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } end def test_successful_purchase @@ -212,6 +221,52 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_three_ds_v2_object_construction + post = {} + @options[:three_d_secure] = @three_ds_secure + + @gateway.send(:add_three_ds, post, @options) + + assert post[:extra] + ds_data = post[:extra] + ds_options = @options[:three_d_secure] + + assert_equal ds_options[:version], ds_data[:threeds_version] + assert_equal ds_options[:cavv], ds_data[:cavv] + assert_equal ds_options[:eci], ds_data[:sli] + assert_equal ds_options[:xid], ds_data[:xid] + assert_equal ds_options[:ds_transaction_id], ds_data[:ds_transaction_id] + assert_equal 'Y', ds_data[:ver] + assert_equal ds_options[:authentication_response_status], ds_data[:par] + end + + def test_purchase_with_three_ds + @options[:three_d_secure] = @three_ds_secure + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + three_ds_params = JSON.parse(data)['extra'] + assert_equal '2.2.0', three_ds_params['threeds_version'] + assert_equal '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', three_ds_params['cavv'] + assert_equal '05', three_ds_params['sli'] + assert_equal 'ODUzNTYzOTcwODU5NzY3Qw==', three_ds_params['xid'] + assert_equal 'Y', three_ds_params['ver'] + assert_equal 'Y', three_ds_params['par'] + end + end + + def test_formatted_enrollment + assert_equal 'Y', @gateway.send('formatted_enrollment', 'Y') + assert_equal 'Y', @gateway.send('formatted_enrollment', 'true') + assert_equal 'Y', @gateway.send('formatted_enrollment', true) + + assert_equal 'N', @gateway.send('formatted_enrollment', 'N') + assert_equal 'N', @gateway.send('formatted_enrollment', 'false') + assert_equal 'N', @gateway.send('formatted_enrollment', false) + + assert_equal 'U', @gateway.send('formatted_enrollment', 'U') + end + private def pre_scrubbed diff --git a/test/unit/gateways/first_pay_json_test.rb b/test/unit/gateways/first_pay_json_test.rb new file mode 100644 index 00000000000..d1d917acab6 --- /dev/null +++ b/test/unit/gateways/first_pay_json_test.rb @@ -0,0 +1,599 @@ +require 'test_helper' + +class FirstPayJsonTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = FirstPayJsonGateway.new( + processor_id: 1234, + merchant_key: 'a91c38c3-7d7f-4d29-acc7-927b4dca0dbe' + ) + + @credit_card = credit_card + @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' + ) + @amount = 100 + + @options = { + order_id: SecureRandom.hex(24), + billing_address: address + } + end + + def test_successful_purchase + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\"transactionAmount\":\"1.00\"/, data) + assert_match(/\"cardNumber\":\"4242424242424242\"/, data) + assert_match(/\"cardExpMonth\":9/, data) + assert_match(/\"cardExpYear\":\"25\"/, data) + assert_match(/\"cvv\":\"123\"/, data) + assert_match(/\"ownerName\":\"Jim Smith\"/, data) + assert_match(/\"ownerStreet\":\"456 My Street\"/, data) + assert_match(/\"ownerCity\":\"Ottawa\"/, data) + assert_match(/\"ownerState\":\"ON\"/, data) + assert_match(/\"ownerZip\":\"K1C2N6\"/, data) + assert_match(/\"ownerCountry\":\"CA\"/, data) + assert_match(/\"processorId\":1234/, data) + assert_match(/\"merchantKey\":\"a91c38c3-7d7f-4d29-acc7-927b4dca0dbe\"/, data) + end.respond_with(successful_purchase_response) + + assert response + assert_instance_of Response, response + assert_success response + assert_equal '31076534', response.authorization + assert_equal 'Approved 735498', response.message + end + + def test_failed_purchase + response = stub_comms do + @gateway.purchase(200, @credit_card, @options) + end.respond_with(failed_purchase_response) + + assert response + assert_instance_of Response, response + assert_failure response + assert_equal '31076656', response.authorization + assert_equal 'Auth Declined', response.message + end + + def test_successful_google_pay_purchase + response = stub_comms do + @gateway.purchase(@amount, @google_pay, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\"walletType\":\"GooglePay\"/, data) + assert_match(/\"paymentCryptogram\":\"EHuWW9PiBkWvqE5juRwDzAUFBAk=\"/, data) + assert_match(/\"eciIndicator\":\"05\"/, data) + assert_match(/\"transactionAmount\":\"1.00\"/, data) + assert_match(/\"cardNumber\":\"4005550000000019\"/, data) + assert_match(/\"cardExpMonth\":2/, data) + assert_match(/\"cardExpYear\":\"35\"/, data) + assert_match(/\"ownerName\":\"Jim Smith\"/, data) + assert_match(/\"ownerStreet\":\"456 My Street\"/, data) + assert_match(/\"ownerCity\":\"Ottawa\"/, data) + assert_match(/\"ownerState\":\"ON\"/, data) + assert_match(/\"ownerZip\":\"K1C2N6\"/, data) + assert_match(/\"ownerCountry\":\"CA\"/, data) + assert_match(/\"processorId\":1234/, data) + assert_match(/\"merchantKey\":\"a91c38c3-7d7f-4d29-acc7-927b4dca0dbe\"/, data) + end.respond_with(successful_purchase_google_pay_response) + + assert response + assert_instance_of Response, response + assert_success response + assert_equal '31079731', response.authorization + assert_equal 'Approved 507983', response.message + end + + def test_successful_apple_pay_purchase + response = stub_comms do + @gateway.purchase(@amount, @apple_pay, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\"walletType\":\"ApplePay\"/, data) + assert_match(/\"paymentCryptogram\":\"EHuWW9PiBkWvqE5juRwDzAUFBAk=\"/, data) + assert_match(/\"eciIndicator\":\"05\"/, data) + assert_match(/\"transactionAmount\":\"1.00\"/, data) + assert_match(/\"cardNumber\":\"4005550000000019\"/, data) + assert_match(/\"cardExpMonth\":2/, data) + assert_match(/\"cardExpYear\":\"35\"/, data) + assert_match(/\"ownerName\":\"Jim Smith\"/, data) + assert_match(/\"ownerStreet\":\"456 My Street\"/, data) + assert_match(/\"ownerCity\":\"Ottawa\"/, data) + assert_match(/\"ownerState\":\"ON\"/, data) + assert_match(/\"ownerZip\":\"K1C2N6\"/, data) + assert_match(/\"ownerCountry\":\"CA\"/, data) + assert_match(/\"processorId\":1234/, data) + assert_match(/\"merchantKey\":\"a91c38c3-7d7f-4d29-acc7-927b4dca0dbe\"/, data) + end.respond_with(successful_purchase_apple_pay_response) + + assert response + assert_instance_of Response, response + assert_success response + assert_equal '31080040', response.authorization + assert_equal 'Approved 576126', response.message + end + + def test_successful_authorize + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/\"transactionAmount\":\"1.00\"/, data) + assert_match(/\"cardNumber\":\"4242424242424242\"/, data) + assert_match(/\"cardExpMonth\":9/, data) + assert_match(/\"cardExpYear\":\"25\"/, data) + assert_match(/\"cvv\":\"123\"/, data) + assert_match(/\"ownerName\":\"Jim Smith\"/, data) + assert_match(/\"ownerStreet\":\"456 My Street\"/, data) + assert_match(/\"ownerCity\":\"Ottawa\"/, data) + assert_match(/\"ownerState\":\"ON\"/, data) + assert_match(/\"ownerZip\":\"K1C2N6\"/, data) + assert_match(/\"ownerCountry\":\"CA\"/, data) + assert_match(/\"processorId\":1234/, data) + assert_match(/\"merchantKey\":\"a91c38c3-7d7f-4d29-acc7-927b4dca0dbe\"/, data) + end.respond_with(successful_authorize_response) + + assert response + assert_instance_of Response, response + assert_success response + assert_equal '31076755', response.authorization + assert_equal 'Approved 487154', response.message + end + + def test_failed_authorize + @gateway.stubs(:ssl_post).returns(failed_authorize_response) + response = @gateway.authorize(@amount, @credit_card, @options) + + assert_failure response + assert_equal '31076792', response.authorization + assert_equal 'Auth Declined', response.message + end + + def test_successful_capture + response = stub_comms do + @gateway.capture(@amount, '31076883') + end.check_request do |_endpoint, data, _headers| + assert_match(/\"transactionAmount\":\"1.00\"/, data) + assert_match(/\"refNumber\":\"31076883\"/, data) + assert_match(/\"processorId\":1234/, data) + assert_match(/\"merchantKey\":\"a91c38c3-7d7f-4d29-acc7-927b4dca0dbe\"/, data) + end.respond_with(successful_capture_response) + + assert response + assert_instance_of Response, response + assert_success response + assert_equal '31076883', response.authorization + assert_equal 'APPROVED', response.message + end + + def test_failed_capture + @gateway.stubs(:ssl_post).returns(failed_capture_response) + response = @gateway.capture(@amount, '1234') + + assert_failure response + assert_equal '1234', response.authorization + assert response.message.include?('Settle Failed') + end + + def test_successful_refund + response = stub_comms do + @gateway.refund(@amount, '31077003') + end.check_request do |_endpoint, data, _headers| + assert_match(/\"transactionAmount\":\"1.00\"/, data) + assert_match(/\"refNumber\":\"31077003\"/, data) + assert_match(/\"processorId\":1234/, data) + assert_match(/\"merchantKey\":\"a91c38c3-7d7f-4d29-acc7-927b4dca0dbe\"/, data) + end.respond_with(successful_refund_response) + + assert response + assert_instance_of Response, response + assert_success response + assert_equal '31077004', response.authorization + assert_equal 'APPROVED', response.message + end + + def test_failed_refund + @gateway.stubs(:ssl_post).returns(failed_refund_response) + response = @gateway.refund(@amount, '1234') + + assert_failure response + assert_equal '', response.authorization + assert response.message.include?('No transaction was found to refund.') + end + + def test_successful_void + response = stub_comms do + @gateway.void('31077140') + end.check_request do |_endpoint, data, _headers| + assert_match(/\"refNumber\":\"31077140\"/, data) + assert_match(/\"processorId\":1234/, data) + assert_match(/\"merchantKey\":\"a91c38c3-7d7f-4d29-acc7-927b4dca0dbe\"/, data) + end.respond_with(successful_void_response) + + assert response + assert_instance_of Response, response + assert_success response + assert_equal '31077142', response.authorization + assert_equal 'APPROVED', response.message + end + + def test_failed_void + @gateway.stubs(:ssl_post).returns(failed_void_response) + response = @gateway.void('1234') + + assert_failure response + assert_equal '', response.authorization + assert response.message.include?('Void Failed. Transaction cannot be voided.') + end + + def test_error_message + @gateway.stubs(:ssl_post).returns(failed_login_response) + response = @gateway.purchase(@amount, @credit_card, @options) + + assert_failure response + assert_equal 'isError', response.error_code + assert response.message.include?('Unable to retrieve merchant information') + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def successful_purchase_response + <<~RESPONSE + { + "data": { + "authResponse": "Approved 735498", + "authCode": "735498", + "referenceNumber": "31076534", + "isPartial": false, + "partialId": "", + "originalFullAmount": 1.0, + "partialAmountApproved": 0.0, + "avsResponse": "Y", + "cvv2Response": "", + "orderId": "638430008263685218", + "cardType": "Visa", + "last4": "1111", + "maskedPan": "411111******1111", + "token": "1266392642841111", + "cardExpMonth": "9", + "cardExpYear": "25", + "hasFee": false, + "fee": null, + "billingAddress": { "ownerName": "Jim Smith", "ownerStreet": "456 My Street", "ownerStreet2": null, "ownerCity": "Ottawa", "ownerState": "ON", "ownerZip": "K1C2N6", "ownerCountry": "CA", "ownerEmail": null, "ownerPhone": null } + }, + "isError": false, + "errorMessages": [], + "validationHasFailed": false, + "validationFailures": [], + "isSuccess": true, + "action": "Sale" + } + RESPONSE + end + + def failed_purchase_response + <<~RESPONSE + { + "data": { + "authResponse": "Auth Declined", + "authCode": "200", + "referenceNumber": "31076656", + "isPartial": false, + "partialId": "", + "originalFullAmount": 2.0, + "partialAmountApproved": 0.0, + "avsResponse": "", + "cvv2Response": "", + "orderId": "", + "cardType": "Visa", + "last4": "1111", + "maskedPan": "411111******1111", + "token": "1266392642841111", + "cardExpMonth": "9", + "cardExpYear": "25", + "hasFee": false, + "fee": null, + "billingAddress": { "ownerName": "Jim Smith", "ownerStreet": "456 My Street", "ownerStreet2": null, "ownerCity": "Ottawa", "ownerState": "ON", "ownerZip": "K1C2N6", "ownerCountry": "CA", "ownerEmail": null, "ownerPhone": null } + }, + "isError": true, + "errorMessages": ["Auth Declined"], + "validationHasFailed": false, + "validationFailures": [], + "isSuccess": false, + "action": "Sale" + } + RESPONSE + end + + def successful_purchase_google_pay_response + <<~RESPONSE + { + "data":{ + "authResponse":"Approved 507983", + "authCode":"507983", + "referenceNumber":"31079731", + "isPartial":false, + "partialId":"", + "originalFullAmount":1.0, + "partialAmountApproved":0.0, + "avsResponse":"Y", + "cvv2Response":"", + "orderId":"bbabd4c3b486eed0935a0e12bf4b000579274dfea330223a", + "cardType":"Visa-GooglePay", + "last4":"0019", + "maskedPan":"400555******0019", + "token":"8257959132340019", + "cardExpMonth":"2", + "cardExpYear":"35", + "hasFee":false, + "fee":null, + "billingAddress":{"ownerName":"Jim Smith", "ownerStreet":"456 My Street", "ownerStreet2":null, "ownerCity":"Ottawa", "ownerState":"ON", "ownerZip":"K1C2N6", "ownerCountry":"CA", "ownerEmail":null, "ownerPhone":null} + }, + "isError":false, + "errorMessages":[], + "validationHasFailed":false, + "validationFailures":[], + "isSuccess":true, + "action":"Sale" + } + RESPONSE + end + + def successful_purchase_apple_pay_response + <<~RESPONSE + { + "data":{ + "authResponse":"Approved 576126", + "authCode":"576126", + "referenceNumber":"31080040", + "isPartial":false, + "partialId":"", + "originalFullAmount":1.0, + "partialAmountApproved":0.0, + "avsResponse":"Y", + "cvv2Response":"", + "orderId":"f6527d4f5ebc29a60662239be0221f612797030cde82d50c", + "cardType":"Visa-ApplePay", + "last4":"0019", + "maskedPan":"400555******0019", + "token":"8257959132340019", + "cardExpMonth":"2", + "cardExpYear":"35", + "hasFee":false, + "fee":null, + "billingAddress":{"ownerName":"Jim Smith", "ownerStreet":"456 My Street", "ownerStreet2":null, "ownerCity":"Ottawa", "ownerState":"ON", "ownerZip":"K1C2N6", "ownerCountry":"CA", "ownerEmail":null, "ownerPhone":null} + }, + "isError":false, + "errorMessages":[], + "validationHasFailed":false, + "validationFailures":[], + "isSuccess":true, + "action":"Sale" + } + RESPONSE + end + + def successful_authorize_response + <<~RESPONSE + { + "data": { + "authResponse": "Approved 487154", + "authCode": "487154", + "referenceNumber": "31076755", + "isPartial": false, + "partialId": "", + "originalFullAmount": 1.0, + "partialAmountApproved": 0.0, + "avsResponse": "Y", + "cvv2Response": "", + "orderId": "638430019493711407", + "cardType": "Visa", + "last4": "1111", + "maskedPan": "411111******1111", + "token": "1266392642841111", + "hasFee": false, + "fee": null + }, + "isError": false, + "errorMessages": [], + "validationHasFailed": false, + "validationFailures": [], + "isSuccess": true, + "action": "Auth" + } + RESPONSE + end + + def failed_authorize_response + <<~RESPONSE + { + "data": { + "authResponse": "Auth Declined", + "authCode": "200", + "referenceNumber": "31076792", + "isPartial": false, + "partialId": "", + "originalFullAmount": 2.0, + "partialAmountApproved": 0.0, + "avsResponse": "", + "cvv2Response": "", + "orderId": "", + "cardType": "Visa", + "last4": "1111", + "maskedPan": "411111******1111", + "token": "1266392642841111", + "hasFee": false, + "fee": null + }, + "isError": true, + "errorMessages": ["Auth Declined"], + "validationHasFailed": false, + "validationFailures": [], + "isSuccess": false, + "action": "Auth" + } + RESPONSE + end + + def successful_capture_response + <<~RESPONSE + { + "data": { + "authResponse": "APPROVED", + "referenceNumber": "31076883", + "settleAmount": "1", + "batchNumber": "20240208" + }, + "isError": false, + "errorMessages": [], + "validationHasFailed": false, + "validationFailures": [], + "isSuccess": true, + "action": "Settle" + } + RESPONSE + end + + def failed_capture_response + <<~RESPONSE + { + "data":{ + "authResponse":"Settle Failed. Transaction cannot be settled. Make sure the settlement amount does not exceed the original auth amount and that is was authorized less than 30 days ago.", + "referenceNumber":"1234", + "settleAmount":"1", + "batchNumber":"20240208" + }, + "isError":true, + "errorMessages":["Settle Failed. Transaction cannot be settled. Make sure the settlement amount does not exceed the original auth amount and that is was authorized less than 30 days ago."], + "validationHasFailed":false, + "validationFailures":[], + "isSuccess":false, + "action":"Settle" + } + RESPONSE + end + + def successful_refund_response + <<~RESPONSE + { + "data":{ + "authResponse":"APPROVED", + "referenceNumber":"31077004", + "parentReferenceNumber":"31077003", + "refundAmount":"1.00", + "refundType":"void" + }, + "isError":false, + "errorMessages":[], + "validationHasFailed":false, + "validationFailures":[], + "isSuccess":true, + "action":"Refund" + } + RESPONSE + end + + def failed_refund_response + <<~RESPONSE + { + "data":{ + "authResponse":"No transaction was found to refund.", + "referenceNumber":"", + "parentReferenceNumber":"", + "refundAmount":"", + "refundType":"void" + }, + "isError":true, + "errorMessages":["No transaction was found to refund."], + "validationHasFailed":false, + "validationFailures":[], + "isSuccess":false, + "action":"Refund" + } + RESPONSE + end + + def successful_void_response + <<~RESPONSE + { + "data":{ + "authResponse":"APPROVED", + "referenceNumber":"31077142", + "parentReferenceNumber":"31077140" + }, + "isError":false, + "errorMessages":[], + "validationHasFailed":false, + "validationFailures":[], + "isSuccess":true, + "action":"Void" + } + RESPONSE + end + + def failed_void_response + <<~RESPONSE + { + "data":{ + "authResponse":"Void Failed. Transaction cannot be voided.", + "referenceNumber":"", + "parentReferenceNumber":"" + }, + "isError":true, + "errorMessages":["Void Failed. Transaction cannot be voided."], + "validationHasFailed":false, + "validationFailures":[], + "isSuccess":false, + "action":"Void" + } + RESPONSE + end + + def failed_login_response + <<~RESPONSE + { + "isError":true, + "errorMessages":["Unable to retrieve merchant information"], + "validationHasFailed":false, + "validationFailures":[], + "isSuccess":false, + "action":"Sale" + } + RESPONSE + end + + def pre_scrubbed + <<~RESPONSE + "opening connection to secure.1stpaygateway.net:443...\nopened\nstarting SSL for secure.1stpaygateway.net:443...\nSSL established, protocol: TLSv1.3, cipher: TLS_AES_128_GCM_SHA256\n<- \"POST /secure/RestGW/Gateway/Transaction/Sale 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: secure.1stpaygateway.net\\r\\nContent-Length: 314\\r\\n\\r\\n\"\n<- \"{\\\"transactionAmount\\\":\\\"1.00\\\",\\\"cardNumber\\\":\\\"4111111111111111\\\",\\\"cardExpMonth\\\":9,\\\"cardExpYear\\\":\\\"25\\\",\\\"cvv\\\":789,\\\"ownerName\\\":\\\"Jim Smith\\\",\\\"ownerStreet\\\":\\\"456 My Street\\\",\\\"ownerCity\\\":\\\"Ottawa\\\",\\\"ownerState\\\":\\\"ON\\\",\\\"ownerZip\\\":\\\"K1C2N6\\\",\\\"ownerCountry\\\":\\\"CA\\\",\\\"processorId\\\":\\\"15417\\\",\\\"merchantKey\\\":\\\"a91c38c3-7d7f-4d29-acc7-927b4dca0dbe\\\"}\"\n-> \"HTTP/1.1 201 Created\\r\\n\"\n-> \"Cache-Control: no-cache\\r\\n\"\n-> \"Pragma: no-cache\\r\\n\"\n-> \"Content-Type: application/json; charset=utf-8\\r\\n\"\n-> \"Expires: -1\\r\\n\"\n-> \"Server: Microsoft-IIS/8.5\\r\\n\"\n-> \"cacheControlHeader: max-age=604800\\r\\n\"\n-> \"X-Frame-Options: SAMEORIGIN\\r\\n\"\n-> \"Server-Timing: dtSInfo;desc=\\\"0\\\", dtRpid;desc=\\\"6653911\\\"\\r\\n\"\n-> \"Set-Cookie: dtCookie=v_4_srv_25_sn_229120735766FEB2E6DDFF943AAE854B_perc_100000_ol_0_mul_1_app-3A9b02c199f0b03d02_1_rcs-3Acss_0; Path=/; Domain=.1stpaygateway.net\\r\\n\"\n-> \"Date: Thu, 08 Feb 2024 16:01:55 GMT\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"Content-Length: 728\\r\\n\"\n-> \"Set-Cookie: visid_incap_1062257=eHvRBa+XQCW1gGR0YBPEY/P6xGUAAAAAQUIPAAAAAACnSZS9oi5gsXdpeLLAD5GF; expires=Fri, 07 Feb 2025 06:54:02 GMT; HttpOnly; path=/; Domain=.1stpaygateway.net\\r\\n\"\n-> \"Set-Cookie: nlbi_1062257=dhZJMDyfcwOqd4xnV7L7rwAAAAC5FWzum6uW3m7ncs3yPd5v; path=/; Domain=.1stpaygateway.net\\r\\n\"\n-> \"Set-Cookie: incap_ses_1431_1062257=KaP3NrSI5RQVmH3mPu/bE/P6xGUAAAAAjL9pVzaGFN+QxtEAMI1qbQ==; path=/; Domain=.1stpaygateway.net\\r\\n\"\n-> \"X-CDN: Imperva\\r\\n\"\n-> \"X-Iinfo: 12-32874223-32874361 NNNN CT(38 76 0) RT(1707408112989 881) q(0 0 1 -1) r(17 17) U24\\r\\n\"\n-> \"\\r\\n\"\nreading 728 bytes...\n-> \"{\\\"data\\\":{\\\"authResponse\\\":\\\"Approved 360176\\\",\\\"authCode\\\":\\\"360176\\\",\\\"referenceNumber\\\":\\\"31077352\\\",\\\"isPartial\\\":false,\\\"partialId\\\":\\\"\\\",\\\"originalFullAmount\\\":1.0,\\\"partialAmountApproved\\\":0.0,\\\"avsResponse\\\":\\\"Y\\\",\\\"cvv2Response\\\":\\\"\\\",\\\"orderId\\\":\\\"638430049144239976\\\",\\\"cardType\\\":\\\"Visa\\\",\\\"last4\\\":\\\"1111\\\",\\\"maskedPan\\\":\\\"411111******1111\\\",\\\"token\\\":\\\"1266392642841111\\\",\\\"cardExpMonth\\\":\\\"9\\\",\\\"cardExpYear\\\":\\\"25\\\",\\\"hasFee\\\":false,\\\"fee\\\":null,\\\"billi\"\n-> \"ngAddress\\\":{\\\"ownerName\\\":\\\"Jim Smith\\\",\\\"ownerStreet\\\":\\\"456 My Street\\\",\\\"ownerStreet2\\\":null,\\\"ownerCity\\\":\\\"Ottawa\\\",\\\"ownerState\\\":\\\"ON\\\",\\\"ownerZip\\\":\\\"K1C2N6\\\",\\\"ownerCountry\\\":\\\"CA\\\",\\\"ownerEmail\\\":null,\\\"ownerPhone\\\":null}},\\\"isError\\\":false,\\\"errorMessages\\\":[],\\\"validationHasFailed\\\":false,\\\"validationFailures\\\":[],\\\"isSuccess\\\":true,\\\"action\\\":\\\"Sale\\\"}\"\nread 728 bytes\nConn close\n" + RESPONSE + end + + def post_scrubbed + <<~RESPONSE + "opening connection to secure.1stpaygateway.net:443...\nopened\nstarting SSL for secure.1stpaygateway.net:443...\nSSL established, protocol: TLSv1.3, cipher: TLS_AES_128_GCM_SHA256\n<- \"POST /secure/RestGW/Gateway/Transaction/Sale 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: secure.1stpaygateway.net\\r\\nContent-Length: 314\\r\\n\\r\\n\"\n<- \"{\\\"transactionAmount\\\":\\\"1.00\\\",\\\"cardNumber\\\":\\\"[FILTERED]\",\\\"cardExpMonth\\\":9,\\\"cardExpYear\\\":\\\"25\\\",\\\"cvv\\\":[FILTERED],\\\"ownerName\\\":\\\"Jim Smith\\\",\\\"ownerStreet\\\":\\\"456 My Street\\\",\\\"ownerCity\\\":\\\"Ottawa\\\",\\\"ownerState\\\":\\\"ON\\\",\\\"ownerZip\\\":\\\"K1C2N6\\\",\\\"ownerCountry\\\":\\\"CA\\\",\\\"processorId\\\":\\\"[FILTERED]\",\\\"merchantKey\\\":\\\"[FILTERED]\"}\"\n-> \"HTTP/1.1 201 Created\\r\\n\"\n-> \"Cache-Control: no-cache\\r\\n\"\n-> \"Pragma: no-cache\\r\\n\"\n-> \"Content-Type: application/json; charset=utf-8\\r\\n\"\n-> \"Expires: -1\\r\\n\"\n-> \"Server: Microsoft-IIS/8.5\\r\\n\"\n-> \"cacheControlHeader: max-age=604800\\r\\n\"\n-> \"X-Frame-Options: SAMEORIGIN\\r\\n\"\n-> \"Server-Timing: dtSInfo;desc=\\\"0\\\", dtRpid;desc=\\\"6653911\\\"\\r\\n\"\n-> \"Set-Cookie: dtCookie=v_4_srv_25_sn_229120735766FEB2E6DDFF943AAE854B_perc_100000_ol_0_mul_1_app-3A9b02c199f0b03d02_1_rcs-3Acss_0; Path=/; Domain=.1stpaygateway.net\\r\\n\"\n-> \"Date: Thu, 08 Feb 2024 16:01:55 GMT\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"Content-Length: 728\\r\\n\"\n-> \"Set-Cookie: visid_incap_1062257=eHvRBa+XQCW1gGR0YBPEY/P6xGUAAAAAQUIPAAAAAACnSZS9oi5gsXdpeLLAD5GF; expires=Fri, 07 Feb 2025 06:54:02 GMT; HttpOnly; path=/; Domain=.1stpaygateway.net\\r\\n\"\n-> \"Set-Cookie: nlbi_1062257=dhZJMDyfcwOqd4xnV7L7rwAAAAC5FWzum6uW3m7ncs3yPd5v; path=/; Domain=.1stpaygateway.net\\r\\n\"\n-> \"Set-Cookie: incap_ses_1431_1062257=KaP3NrSI5RQVmH3mPu/bE/P6xGUAAAAAjL9pVzaGFN+QxtEAMI1qbQ==; path=/; Domain=.1stpaygateway.net\\r\\n\"\n-> \"X-CDN: Imperva\\r\\n\"\n-> \"X-Iinfo: 12-32874223-32874361 NNNN CT(38 76 0) RT(1707408112989 881) q(0 0 1 -1) r(17 17) U24\\r\\n\"\n-> \"\\r\\n\"\nreading 728 bytes...\n-> \"{\\\"data\\\":{\\\"authResponse\\\":\\\"Approved 360176\\\",\\\"authCode\\\":\\\"360176\\\",\\\"referenceNumber\\\":\\\"31077352\\\",\\\"isPartial\\\":false,\\\"partialId\\\":\\\"\\\",\\\"originalFullAmount\\\":1.0,\\\"partialAmountApproved\\\":0.0,\\\"avsResponse\\\":\\\"Y\\\",\\\"cvv2Response\\\":\\\"\\\",\\\"orderId\\\":\\\"638430049144239976\\\",\\\"cardType\\\":\\\"Visa\\\",\\\"last4\\\":\\\"1111\\\",\\\"maskedPan\\\":\\\"411111******1111\\\",\\\"token\\\":\\\"1266392642841111\\\",\\\"cardExpMonth\\\":\\\"9\\\",\\\"cardExpYear\\\":\\\"25\\\",\\\"hasFee\\\":false,\\\"fee\\\":null,\\\"billi\"\n-> \"ngAddress\\\":{\\\"ownerName\\\":\\\"Jim Smith\\\",\\\"ownerStreet\\\":\\\"456 My Street\\\",\\\"ownerStreet2\\\":null,\\\"ownerCity\\\":\\\"Ottawa\\\",\\\"ownerState\\\":\\\"ON\\\",\\\"ownerZip\\\":\\\"K1C2N6\\\",\\\"ownerCountry\\\":\\\"CA\\\",\\\"ownerEmail\\\":null,\\\"ownerPhone\\\":null}},\\\"isError\\\":false,\\\"errorMessages\\\":[],\\\"validationHasFailed\\\":false,\\\"validationFailures\\\":[],\\\"isSuccess\\\":true,\\\"action\\\":\\\"Sale\\\"}\"\nread 728 bytes\nConn close\n" + RESPONSE + end +end 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/flex_charge_test.rb b/test/unit/gateways/flex_charge_test.rb new file mode 100644 index 00000000000..4d0acc69b80 --- /dev/null +++ b/test/unit/gateways/flex_charge_test.rb @@ -0,0 +1,538 @@ +require 'test_helper' + +class FlexChargeTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = FlexChargeGateway.new( + app_key: 'SOMECREDENTIAL', + app_secret: 'SOMECREDENTIAL', + site_id: 'SOMECREDENTIAL', + mid: 'SOMECREDENTIAL' + ) + @credit_card = credit_card + @amount = 100 + + @options = { + is_declined: true, + order_id: SecureRandom.uuid, + idempotency_key: SecureRandom.uuid, + email: 'test@gmail.com', + response_code: '100', + response_code_source: 'nmi', + avs_result_code: '200', + cvv_result_code: '111', + cavv_result_code: '111', + timezone_utc_offset: '-5', + billing_address: address.merge(name: 'Cure Tester') + } + + @cit_options = { + is_mit: false, + phone: '+99.2001a/+99.2001b' + }.merge(@options) + + @mit_options = { + is_mit: true, + is_recurring: false, + mit_expiry_date_utc: (Time.now + 1.day).getutc.iso8601, + description: 'MyShoesStore' + }.merge(@options) + + @mit_recurring_options = { + is_recurring: true, + subscription_id: SecureRandom.uuid, + subscription_interval: 'monthly' + }.merge(@mit_options) + + @three_d_secure_options = { + three_d_secure: { + eci: '05', + cavv: 'AAABCSIIAAAAAAACcwgAEMCoNh=', + xid: 'MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=', + version: '2.1.0', + ds_transaction_id: 'MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=', + cavv_algorithm: 'AAABCSIIAAAAAAACcwgAEMCoNh=', + directory_response_status: 'Y', + authentication_response_status: 'Y', + enrolled: 'Y' + } + }.merge(@options) + end + + def test_supported_countries + assert_equal %w(US), FlexChargeGateway.supported_countries + end + + def test_supported_cardtypes + assert_equal %i[visa master american_express discover], @gateway.supported_cardtypes + end + + def test_build_request_url_for_purchase + action = :purchase + assert_equal @gateway.send(:url, action), "#{@gateway.test_url}evaluate" + end + + def test_build_request_url_with_id_param + action = :refund + id = 123 + assert_equal @gateway.send(:url, action, id), "#{@gateway.test_url}orders/123/refund" + end + + def test_build_request_url_for_store + action = :store + assert_equal @gateway.send(:url, action), "#{@gateway.test_url}tokenize" + end + + def test_invalid_instance + error = assert_raises(ArgumentError) { FlexChargeGateway.new } + assert_equal 'Missing required parameter: app_key', error.message + end + + def test_successful_purchase + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, headers| + request = JSON.parse(data) + if /token/.match?(endpoint) + assert_equal request['AppKey'], @gateway.options[:app_key] + assert_equal request['AppSecret'], @gateway.options[:app_secret] + end + + if /evaluate/.match?(endpoint) + assert_equal headers['Authorization'], "Bearer #{@gateway.options[:access_token]}" + assert_equal request['siteId'], @gateway.options[:site_id] + assert_equal request['mid'], @gateway.options[:mid] + assert_equal request['isDeclined'], @options[:is_declined] + assert_equal request['orderId'], @options[:order_id] + assert_equal request['idempotencyKey'], @options[:idempotency_key] + assert_equal request['transaction']['timezoneUtcOffset'], @options[:timezone_utc_offset] + assert_equal request['transaction']['amount'], @amount + assert_equal request['transaction']['responseCode'], @options[:response_code] + assert_equal request['transaction']['responseCodeSource'], @options[:response_code_source] + assert_equal request['transaction']['avsResultCode'], @options[:avs_result_code] + assert_equal request['transaction']['cvvResultCode'], @options[:cvv_result_code] + assert_equal request['transaction']['cavvResultCode'], @options[:cavv_result_code] + assert_equal request['payer']['email'], @options[:email] + assert_equal request['description'], @options[:description] + end + end.respond_with(successful_access_token_response, successful_purchase_response) + + assert_success response + + assert_equal 'ca7bb327-a750-412d-a9c3-050d72b3f0c5', response.authorization + assert response.test? + end + + def test_successful_purchase_three_ds_global + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @three_d_secure_options) + end.respond_with(successful_access_token_response, successful_purchase_response) + assert_success response + assert_equal 'ca7bb327-a750-412d-a9c3-050d72b3f0c5', response.authorization + assert response.test? + end + + def test_succeful_request_with_three_ds_global + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @three_d_secure_options) + end.check_request do |_method, endpoint, data, _headers| + if /evaluate/.match?(endpoint) + request = JSON.parse(data) + assert_equal request['threeDSecure']['EcommerceIndicator'], @three_d_secure_options[:three_d_secure][:eci] + assert_equal request['threeDSecure']['authenticationValue'], @three_d_secure_options[:three_d_secure][:cavv] + assert_equal request['threeDSecure']['xid'], @three_d_secure_options[:three_d_secure][:xid] + assert_equal request['threeDSecure']['threeDsVersion'], @three_d_secure_options[:three_d_secure][:version] + assert_equal request['threeDSecure']['directoryServerTransactionId'], @three_d_secure_options[:three_d_secure][:ds_transaction_id] + assert_equal request['threeDSecure']['authenticationValueAlgorithm'], @three_d_secure_options[:three_d_secure][:cavv_algorithm] + assert_equal request['threeDSecure']['directoryResponseStatus'], @three_d_secure_options[:three_d_secure][:directory_response_status] + assert_equal request['threeDSecure']['authenticationResponseStatus'], @three_d_secure_options[:three_d_secure][:authentication_response_status] + assert_equal request['threeDSecure']['enrolled'], @three_d_secure_options[:three_d_secure][:enrolled] + end + end.respond_with(successful_access_token_response, successful_purchase_response) + end + + def test_failed_purchase + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(successful_access_token_response, failed_purchase_response) + + assert_failure response + assert_equal '400', response.error_code + assert_equal '400', response.message + end + + def test_failed_refund + response = stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, 'reference', @options) + end.check_request do |_method, endpoint, data, _headers| + request = JSON.parse(data) + + if /token/.match?(endpoint) + assert_equal request['AppKey'], @gateway.options[:app_key] + assert_equal request['AppSecret'], @gateway.options[:app_secret] + end + + assert_equal request['amountToRefund'], (@amount.to_f / 100).round(2) if /orders\/reference\/refund/.match?(endpoint) + end.respond_with(successful_access_token_response, failed_refund_response) + + assert_failure response + assert response.test? + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_address_names_from_address + names = @gateway.send(:address_names, @options[:billing_address][:name], @credit_card) + + assert_equal 'Cure', names.first + assert_equal 'Tester', names.last + end + + def test_address_names_from_credit_card + names = @gateway.send(:address_names, 'Doe', @credit_card) + + assert_equal 'Longbob', names.first + assert_equal 'Doe', names.last + end + + def test_successful_store + response = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.respond_with(successful_access_token_response, successful_store_response) + + assert_success response + assert_equal 'd3e10716-6aac-4eb8-a74d-c1a3027f1d96', response.authorization + end + + def test_successful_inquire_request + session_id = 'f8da8dc7-17de-4b5e-858d-4bdc47cd5dbf' + stub_comms(@gateway, :ssl_request) do + @gateway.inquire(session_id, {}) + end.check_request do |_method, endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['orderSessionKey'], session_id if /outcome/.match?(endpoint) + end.respond_with(successful_access_token_response, successful_purchase_response) + end + + private + + def pre_scrubbed + "opening connection to api-sandbox.flex-charge.com:443... + opened + starting SSL for api-sandbox.flex-charge.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_128_GCM_SHA256 + <- \"POST /v1/oauth2/token HTTP/1.1\\r\ + Content-Type: application/json\\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-sandbox.flex-charge.com\\r\ + Content-Length: 153\\r\ + \\r\ + \" + <- \"{\\\"AppKey\\\":\\\"2/tprAqlvujvIZonWkLntQMj3CbH7Y9sKLqTTdWu\\\",\\\"AppSecret\\\":\\\"AQAAAAEAACcQAAAAEFb/TYEfAlzWhb6SDXEbS06A49kc/P6Cje6 MDta3o61GGS4tLLk8m/BZuJOyZ7B99g==\\\"}\" + -> \"HTTP/1.1 200 OK\\r\ + \" + -> \"Date: Thu, 04 Apr 2024 13:29:08 GMT\\r\ + \" + -> \"Content-Type: application/json; charset=utf-8\\r\ + \" + -> \"Content-Length: 902\\r\ + \" + -> \"Connection: close\\r\ + \" + -> \"server: Kestrel\\r\ + \" + -> \"set-cookie: AWSALB=n2vt9daKLxUPgxF+n3g+4uQDgxt1PNVOY/HwVuLZdkf0Ye8XkAFuEVrnu6xh/xf7k2ZYZHqaPthqR36D3JxPJIs7QfNbcfAhvxTlPEVx8t/IyB1Kb/Vinasi3vZD; Expires=Thu, 11 Apr 2024 13:29:08 GMT; Path=/\\r\ + \" + -> \"set-cookie: AWSALBCORS=n2vt9daKLxUPgxF+n3g+4uQDgxt1PNVOY/HwVuLZdkf0Ye8XkAFuEVrnu6xh/xf7k2ZYZHqaPthqR36D3JxPJIs7QfNbcfAhvxTlPEVx8t/IyB1Kb/Vinasi3vZD; Expires=Thu, 11 Apr 2024 13:29:08 GMT; Path=/; SameSite=None; Secure\\r\ + \" + -> \"apigw-requestid: Vs-twgfMoAMEaEQ=\\r\ + \" + -> \"\\r\ + \" + reading 902 bytes... + -> \"{\\\"accessToken\\\":\\\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwYmE4NGY2ZS03YTllLTQzZjEtYWU2ZC1jNTA4YjQ2NjQyNGEiLCJ1bmlxdWVfbmFtZSI6IjBiYTg0ZjZlLTdhOWUtNDNmMS1hZTZkLWM1MDhiNDY2NDI0YSIsImp0aSI6IjI2NTQxY2FlLWM3ZjUtNDU0MC04MTUyLTZiNGExNzQ3ZTJmMSIsImlhdCI6IjE3MTIyMzczNDg1NjUiLCJhdWQiOlsicGF5bWVudHMiLCJvcmRlcnMiLCJtZXJjaGFudHMiLCJlbGlnaWJpbGl0eS1zZnRwIiwiZWxpZ2liaWxpdHkiLCJjb250YWN0Il0sImN1c3RvbTptaWQiOiJkOWQwYjVmZC05NDMzLTQ0ZDMtODA1MS02M2ZlZTI4NzY4ZTgiLCJuYmYiOjE3MTIyMzczNDgsImV4cCI6MTcxMjIzNzk0OCwiaXNzIjoiQXBpLUNsaWVudC1TZXJ2aWNlIn0.ZGYzd6NA06o2zP-qEWf6YpyrY-v-Jb-i1SGUOUkgRPo\\\",\\\"refreshToken\\\":\\\"AQAAAAEAACcQAAAAEG5H7emaTnpUcVSWrbwLlPBEEdQ3mTCCHT5YMLBNauXxilaXHwL8oFiI4heg6yA\\\",\\\"expires\\\":1712237948565,\\\"id\\\":\\\"0ba84f6e-7a9e-43f1-ae6d-c508b466424a\\\",\\\"session\\\":null,\\\"daysToEnforceMFA\\\":null,\\\"skipAvailable\\\":null,\\\"success\\\":true,\\\"result\\\":null,\\\"status\\\":null,\\\"statusCode\\\":null,\\\"errors\\\":[],\\\"customProperties\\\":{}}\" + read 902 bytes + Conn close + opening connection to api-sandbox.flex-charge.com:443... + opened + starting SSL for api-sandbox.flex-charge.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_128_GCM_SHA256 + <- \"POST /v1/evaluate HTTP/1.1\\r\ + Content-Type: application/json\\r\ + Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwYmE4NGY2ZS03YTllLTQzZjEtYWU2ZC1jNTA4YjQ2NjQyNGEiLCJ1bmlxdWVfbmFtZSI6IjBiYTg0ZjZlLTdhOWUtNDNmMS1hZTZkLWM1MDhiNDY2NDI0YSIsImp0aSI6IjI2NTQxY2FlLWM3ZjUtNDU0MC04MTUyLTZiNGExNzQ3ZTJmMSIsImlhdCI6IjE3MTIyMzczNDg1NjUiLCJhdWQiOlsicGF5bWVudHMiLCJvcmRlcnMiLCJtZXJjaGFudHMiLCJlbGlnaWJpbGl0eS1zZnRwIiwiZWxpZ2liaWxpdHkiLCJjb250YWN0Il0sImN1c3RvbTptaWQiOiJkOWQwYjVmZC05NDMzLTQ0ZDMtODA1MS02M2ZlZTI4NzY4ZTgiLCJuYmYiOjE3MTIyMzczNDgsImV4cCI6MTcxMjIzNzk0OCwiaXNzIjoiQXBpLUNsaWVudC1TZXJ2aWNlIn0.ZGYzd6NA06o2zP-qEWf6YpyrY-v-Jb-i1SGUOUkgRPo\\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-sandbox.flex-charge.com\\r\ + Content-Length: 999\\r\ + \\r\ + \" + <- \"{\\\"siteId\\\":\\\"ffae80fd-2b8e-487a-94c3-87503a0c71bb\\\",\\\"mid\\\":\\\"d9d0b5fd-9433-44d3-8051-63fee28768e8\\\",\\\"isDeclined\\\":true,\\\"orderId\\\":\\\"b53827df-1f19-4dd9-9829-25a108255ba1\\\",\\\"idempotencyKey\\\":\\\"46902e30-ae70-42c5-a0d3-1994133b4f52\\\",\\\"transaction\\\":{\\\"id\\\":\\\"b53827df-1f19-4dd9-9829-25a108255ba1\\\",\\\"dynamicDescriptor\\\":\\\"MyShoesStore\\\",\\\"timezoneUtcOffset\\\":\\\"-5\\\",\\\"amount\\\":100,\\\"currency\\\":\\\"USD\\\",\\\"responseCode\\\":\\\"100\\\",\\\"responseCodeSource\\\":\\\"nmi\\\",\\\"avsResultCode\\\":\\\"200\\\",\\\"cvvResultCode\\\":\\\"111\\\",\\\"cavvResultCode\\\":\\\"111\\\",\\\"cardNotPresent\\\":true},\\\"paymentMethod\\\":{\\\"holderName\\\":\\\"Longbob Longsen\\\",\\\"cardType\\\":\\\"CREDIT\\\",\\\"cardBrand\\\":\\\"VISA\\\",\\\"cardCountry\\\":\\\"CA\\\",\\\"expirationMonth\\\":9,\\\"expirationYear\\\":2025,\\\"cardBinNumber\\\":\\\"411111\\\",\\\"cardLast4Digits\\\":\\\"1111\\\",\\\"cardNumber\\\":\\\"4111111111111111\\\"},\\\"billingInformation\\\":{\\\"firstName\\\":\\\"Cure\\\",\\\"lastName\\\":\\\"Tester\\\",\\\"country\\\":\\\"CA\\\",\\\"phone\\\":\\\"(555)555-5555\\\",\\\"countryCode\\\":\\\"CA\\\",\\\"addressLine1\\\":\\\"456 My Street\\\",\\\"state\\\":\\\"ON\\\",\\\"city\\\":\\\"Ottawa\\\",\\\"zipCode\\\":\\\"K1C2N6\\\"},\\\"payer\\\":{\\\"email\\\":\\\"test@gmail.com\\\",\\\"phone\\\":\\\"+99.2001a/+99.2001b\\\"}}\" + -> \"HTTP/1.1 200 OK\\r\ + \" + -> \"Date: Thu, 04 Apr 2024 13:29:11 GMT\\r\ + \" + -> \"Content-Type: application/json; charset=utf-8\\r\ + \" + -> \"Content-Length: 230\\r\ + \" + -> \"Connection: close\\r\ + \" + -> \"server: Kestrel\\r\ + \" + -> \"set-cookie: AWSALB=Mw7gQis/D9qOm0eQvpkNsEOvZerr+YBDNyfJyJ2T2BGel3cg8AX9OtpuXXR/UCCgNRf5J9UTY+soHqLEJuxIEdEK5lNPelLtQbO0oKGB12q0gPRI7T5H1ijnf+RF; Expires=Thu, 11 Apr 2024 13:29:08 GMT; Path=/\\r\ + \" + -> \"set-cookie: AWSALBCORS=Mw7gQis/D9qOm0eQvpkNsEOvZerr+YBDNyfJyJ2T2BGel3cg8AX9OtpuXXR/UCCgNRf5J9UTY+soHqLEJuxIEdEK5lNPelLtQbO0oKGB12q0gPRI7T5H1ijnf+RF; Expires=Thu, 11 Apr 2024 13:29:08 GMT; Path=/; SameSite=None; Secure\\r\ + \" + -> \"apigw-requestid: Vs-t0g9gIAMES8w=\\r\ + \" + -> \"\\r\ + \" + reading 230 bytes... + -> \"{\\\"orderSessionKey\\\":\\\"e97b1ff1-4449-46da-bc6c-a76d23f16353\\\",\\\"senseKey\\\":null,\\\"orderId\\\":\\\"e97b1ff1-4449-46da-bc6c-a76d23f16353\\\",\\\"success\\\":true,\\\"result\\\":\\\"Success\\\",\\\"status\\\":\\\"CHALLENGE\\\",\\\"statusCode\\\":null,\\\"errors\\\":[],\\\"customProperties\\\":{}}\" + read 230 bytes + Conn close + " + end + + def post_scrubbed + "opening connection to api-sandbox.flex-charge.com:443... + opened + starting SSL for api-sandbox.flex-charge.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_128_GCM_SHA256 + <- \"POST /v1/oauth2/token HTTP/1.1\\r\ + Content-Type: application/json\\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-sandbox.flex-charge.com\\r\ + Content-Length: 153\\r\ + \\r\ + \" + <- \"{\\\"AppKey\\\":\\\"[FILTERED]\",\\\"AppSecret\\\":\\\"[FILTERED]\"}\" + -> \"HTTP/1.1 200 OK\\r\ + \" + -> \"Date: Thu, 04 Apr 2024 13:29:08 GMT\\r\ + \" + -> \"Content-Type: application/json; charset=utf-8\\r\ + \" + -> \"Content-Length: 902\\r\ + \" + -> \"Connection: close\\r\ + \" + -> \"server: Kestrel\\r\ + \" + -> \"set-cookie: AWSALB=n2vt9daKLxUPgxF+n3g+4uQDgxt1PNVOY/HwVuLZdkf0Ye8XkAFuEVrnu6xh/xf7k2ZYZHqaPthqR36D3JxPJIs7QfNbcfAhvxTlPEVx8t/IyB1Kb/Vinasi3vZD; Expires=Thu, 11 Apr 2024 13:29:08 GMT; Path=/\\r\ + \" + -> \"set-cookie: AWSALBCORS=n2vt9daKLxUPgxF+n3g+4uQDgxt1PNVOY/HwVuLZdkf0Ye8XkAFuEVrnu6xh/xf7k2ZYZHqaPthqR36D3JxPJIs7QfNbcfAhvxTlPEVx8t/IyB1Kb/Vinasi3vZD; Expires=Thu, 11 Apr 2024 13:29:08 GMT; Path=/; SameSite=None; Secure\\r\ + \" + -> \"apigw-requestid: Vs-twgfMoAMEaEQ=\\r\ + \" + -> \"\\r\ + \" + reading 902 bytes... + -> \"{\\\"accessToken\\\":\\\"[FILTERED]\",\\\"refreshToken\\\":\\\"AQAAAAEAACcQAAAAEG5H7emaTnpUcVSWrbwLlPBEEdQ3mTCCHT5YMLBNauXxilaXHwL8oFiI4heg6yA\\\",\\\"expires\\\":1712237948565,\\\"id\\\":\\\"0ba84f6e-7a9e-43f1-ae6d-c508b466424a\\\",\\\"session\\\":null,\\\"daysToEnforceMFA\\\":null,\\\"skipAvailable\\\":null,\\\"success\\\":true,\\\"result\\\":null,\\\"status\\\":null,\\\"statusCode\\\":null,\\\"errors\\\":[],\\\"customProperties\\\":{}}\" + read 902 bytes + Conn close + opening connection to api-sandbox.flex-charge.com:443... + opened + starting SSL for api-sandbox.flex-charge.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_128_GCM_SHA256 + <- \"POST /v1/evaluate 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-sandbox.flex-charge.com\\r\ + Content-Length: 999\\r\ + \\r\ + \" + <- \"{\\\"siteId\\\":\\\"[FILTERED]\",\\\"mid\\\":\\\"[FILTERED]\",\\\"isDeclined\\\":true,\\\"orderId\\\":\\\"b53827df-1f19-4dd9-9829-25a108255ba1\\\",\\\"idempotencyKey\\\":\\\"46902e30-ae70-42c5-a0d3-1994133b4f52\\\",\\\"transaction\\\":{\\\"id\\\":\\\"b53827df-1f19-4dd9-9829-25a108255ba1\\\",\\\"dynamicDescriptor\\\":\\\"MyShoesStore\\\",\\\"timezoneUtcOffset\\\":\\\"-5\\\",\\\"amount\\\":100,\\\"currency\\\":\\\"USD\\\",\\\"responseCode\\\":\\\"100\\\",\\\"responseCodeSource\\\":\\\"nmi\\\",\\\"avsResultCode\\\":\\\"200\\\",\\\"cvvResultCode\\\":\\\"111\\\",\\\"cavvResultCode\\\":\\\"111\\\",\\\"cardNotPresent\\\":true},\\\"paymentMethod\\\":{\\\"holderName\\\":\\\"Longbob Longsen\\\",\\\"cardType\\\":\\\"CREDIT\\\",\\\"cardBrand\\\":\\\"VISA\\\",\\\"cardCountry\\\":\\\"CA\\\",\\\"expirationMonth\\\":9,\\\"expirationYear\\\":2025,\\\"cardBinNumber\\\":\\\"411111\\\",\\\"cardLast4Digits\\\":\\\"1111\\\",\\\"cardNumber\\\":\\\"[FILTERED]\"},\\\"billingInformation\\\":{\\\"firstName\\\":\\\"Cure\\\",\\\"lastName\\\":\\\"Tester\\\",\\\"country\\\":\\\"CA\\\",\\\"phone\\\":\\\"(555)555-5555\\\",\\\"countryCode\\\":\\\"CA\\\",\\\"addressLine1\\\":\\\"456 My Street\\\",\\\"state\\\":\\\"ON\\\",\\\"city\\\":\\\"Ottawa\\\",\\\"zipCode\\\":\\\"K1C2N6\\\"},\\\"payer\\\":{\\\"email\\\":\\\"test@gmail.com\\\",\\\"phone\\\":\\\"+99.2001a/+99.2001b\\\"}}\" + -> \"HTTP/1.1 200 OK\\r\ + \" + -> \"Date: Thu, 04 Apr 2024 13:29:11 GMT\\r\ + \" + -> \"Content-Type: application/json; charset=utf-8\\r\ + \" + -> \"Content-Length: 230\\r\ + \" + -> \"Connection: close\\r\ + \" + -> \"server: Kestrel\\r\ + \" + -> \"set-cookie: AWSALB=Mw7gQis/D9qOm0eQvpkNsEOvZerr+YBDNyfJyJ2T2BGel3cg8AX9OtpuXXR/UCCgNRf5J9UTY+soHqLEJuxIEdEK5lNPelLtQbO0oKGB12q0gPRI7T5H1ijnf+RF; Expires=Thu, 11 Apr 2024 13:29:08 GMT; Path=/\\r\ + \" + -> \"set-cookie: AWSALBCORS=Mw7gQis/D9qOm0eQvpkNsEOvZerr+YBDNyfJyJ2T2BGel3cg8AX9OtpuXXR/UCCgNRf5J9UTY+soHqLEJuxIEdEK5lNPelLtQbO0oKGB12q0gPRI7T5H1ijnf+RF; Expires=Thu, 11 Apr 2024 13:29:08 GMT; Path=/; SameSite=None; Secure\\r\ + \" + -> \"apigw-requestid: Vs-t0g9gIAMES8w=\\r\ + \" + -> \"\\r\ + \" + reading 230 bytes... + -> \"{\\\"orderSessionKey\\\":\\\"e97b1ff1-4449-46da-bc6c-a76d23f16353\\\",\\\"senseKey\\\":null,\\\"orderId\\\":\\\"e97b1ff1-4449-46da-bc6c-a76d23f16353\\\",\\\"success\\\":true,\\\"result\\\":\\\"Success\\\",\\\"status\\\":\\\"CHALLENGE\\\",\\\"statusCode\\\":null,\\\"errors\\\":[],\\\"customProperties\\\":{}}\" + read 230 bytes + Conn close + " + end + + def successful_access_token_response + <<~RESPONSE + { + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwYmE4NGY2ZS03YTllLTQzZjEtYWU2ZC1jNTA4YjQ2NjQyNGEiLCJ1bmlxdWVfbmFtZSI6IjBiYTg0ZjZlLTdhOWUtNDNmMS1hZTZkLWM1MDhiNDY2NDI0YSIsImp0aSI6ImY5NzdlZDE3LWFlZDItNGIxOC1hMjY1LWY0NzkwNTY0ZDc1NSIsImlhdCI6IjE3MTIwNzE1NDMyNDYiLCJhdWQiOlsicGF5bWVudHMiLCJvcmRlcnMiLCJtZXJjaGFudHMiLCJlbGlnaWJpbGl0eS1zZnRwIiwiZWxpZ2liaWxpdHkiLCJjb250YWN0Il0sImN1c3RvbTptaWQiOiJkOWQwYjVmZC05NDMzLTQ0ZDMtODA1MS02M2ZlZTI4NzY4ZTgiLCJuYmYiOjE3MTIwNzE1NDMsImV4cCI6MTcxMjA3MjE0MywiaXNzIjoiQXBpLUNsaWVudC1TZXJ2aWNlIn0.S9xgOejudB93Gf9Np9S8jtudhbY9zJj_j7n5al_SKZg", + "refreshToken": "AQAAAAEAACcQAAAAEKd3NvUOrqgJXW8FtE22UbdZzuMWcbq7kSMIGss9OcV2aGzCXMNrOJgAW5Zg", + "expires": #{(DateTime.now + 10.minutes).strftime('%Q').to_i}, + "id": "0ba84f6e-7a9e-43f1-ae6d-c508b466424a", + "session": null, + "daysToEnforceMFA": null, + "skipAvailable": null, + "success": true, + "result": null, + "status": null, + "statusCode": null, + "errors": [], + "customProperties": {} + } + RESPONSE + end + + def successful_purchase_response + <<~RESPONSE + { + "orderSessionKey": "ca7bb327-a750-412d-a9c3-050d72b3f0c5", + "senseKey": null, + "orderId": "ca7bb327-a750-412d-a9c3-050d72b3f0c5", + "success": true, + "result": "Success", + "status": "CHALLENGE", + "statusCode": null, + "errors": [], + "customProperties": {} + } + RESPONSE + end + + def successful_store_response + <<~RESPONSE + { + "transaction": { + "on_test_gateway": true, + "created_at": "2024-05-14T13:44:25.3179186Z", + "updated_at": "2024-05-14T13:44:25.3179187Z", + "succeeded": true, + "state": null, + "token": null, + "transaction_type": null, + "order_id": null, + "ip": null, + "description": null, + "email": null, + "merchant_name_descriptor": null, + "merchant_location_descriptor": null, + "gateway_specific_fields": null, + "gateway_specific_response_fields": null, + "gateway_transaction_id": null, + "gateway_latency_ms": null, + "amount": 0, + "currency_code": null, + "retain_on_success": null, + "payment_method_added": false, + "message_key": null, + "message": null, + "response": null, + "payment_method": { + "token": "d3e10716-6aac-4eb8-a74d-c1a3027f1d96", + "created_at": "2024-05-14T13:44:25.3179205Z", + "updated_at": "2024-05-14T13:44:25.3179206Z", + "email": null, + "data": null, + "storage_state": null, + "test": false, + "metadata": null, + "last_four_digits": "1111", + "first_six_digits": "41111111", + "card_type": null, + "first_name": "Cure", + "last_name": "Tester", + "month": 9, + "year": 2025, + "address1": null, + "address2": null, + "city": null, + "state": null, + "zip": null, + "country": null, + "phone_number": null, + "company": null, + "full_name": null, + "payment_method_type": null, + "errors": null, + "fingerprint": null, + "verification_value": null, + "number": null + } + }, + "cardBinInfo": null, + "success": true, + "result": null, + "status": null, + "statusCode": null, + "errors": [], + "customProperties": {}, + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwYmE4NGY2ZS03YTllLTQzZjEtYWU2ZC1jNTA4YjQ2NjQyNGEiLCJ1bmlxdWVfbmFtZSI6IjBiYTg0ZjZlLTdhOWUtNDNmMS1hZTZkLWM1MDhiNDY2NDI0YSIsImp0aSI6IjczZTVkOGZiLWYxMDMtNGVlYy1iYTAzLTM2MmY1YjA5MmNkMCIsImlhdCI6IjE3MTU2OTQyNjQ3MDMiLCJhdWQiOlsicGF5bWVudHMiLCJvcmRlcnMiLCJtZXJjaGFudHMiLCJlbGlnaWJpbGl0eS1zZnRwIiwiZWxpZ2liaWxpdHkiLCJjb250YWN0Il0sImN1c3RvbTptaWQiOiJkOWQwYjVmZC05NDMzLTQ0ZDMtODA1MS02M2ZlZTI4NzY4ZTgiLCJuYmYiOjE3MTU2OTQyNjQsImV4cCI6MTcxNTY5NDg2NCwiaXNzIjoiQXBpLUNsaWVudC1TZXJ2aWNlIn0.oB9xtWGthG6tcDie8Q3fXPc1fED8pBAlv8yZQuoiEkA", + "token_expires": 1715694864703 + } + RESPONSE + end + + def failed_purchase_response + <<~RESPONSE + { + "status": "400", + "errors": { + "OrderId": ["Merchant's orderId is required"], + "TraceId": ["00-3b4af05c51be4aa7dd77104ac75f252b-004c728c64ca280d-01"], + "IsDeclined": ["The IsDeclined field is required."], + "IdempotencyKey": ["The IdempotencyKey field is required."], + "Transaction.Id": ["The Id field is required."], + "Transaction.ResponseCode": ["The ResponseCode field is required."], + "Transaction.AvsResultCode": ["The AvsResultCode field is required."], + "Transaction.CvvResultCode": ["The CvvResultCode field is required."] + } + } + RESPONSE + end + + def failed_refund_response + <<~RESPONSE + { + "responseCode": "2001", + "responseMessage": "Amount to refund (1.00) is greater than maximum refund amount in (0.00))", + "transactionId": null, + "success": false, + "result": null, + "status": "FAILED", + "statusCode": null, + "errors": [ + { + "item1": "Amount to refund (1.00) is greater than maximum refund amount in (0.00))", + "item2": "2001", + "item3": "2001", + "item4": true + } + ], + "customProperties": {} + } + RESPONSE + end +end diff --git a/test/unit/gateways/forte_test.rb b/test/unit/gateways/forte_test.rb index 2d3254244db..fb448d46abb 100644 --- a/test/unit/gateways/forte_test.rb +++ b/test/unit/gateways/forte_test.rb @@ -192,6 +192,7 @@ def test_scrub class MockedResponse attr_reader :code, :body + def initialize(body, code = 200) @code = code @body = body 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 0e888bff9d3..964e2739e83 100644 --- a/test/unit/gateways/global_collect_test.rb +++ b/test/unit/gateways/global_collect_test.rb @@ -9,26 +9,21 @@ def setup secret_api_key: '109H/288H*50Y18W4/0G8571F245KA=') @credit_card = credit_card('4567350000427977') - @apple_pay_network_token = network_tokenization_credit_card('4444333322221111', + @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) + source: :apple_pay + ) - @google_pay_network_token = network_tokenization_credit_card('4444333322221111', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - month: '01', - year: Time.new.year + 2, + @google_pay_network_token = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ source: :google_pay, - transaction_id: '123456789', - eci: '05') - - @google_pay_pan_only = credit_card('4444333322221111', - month: '01', - year: Time.new.year + 2) + payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}" + }) @declined_card = credit_card('5424180279791732') @accepted_amount = 4005 @@ -46,7 +41,8 @@ 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 @@ -79,7 +75,7 @@ def test_successful_preproduction_url stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card) end.check_request do |_method, endpoint, _data, _headers| - assert_match(/world\.preprod\.api-ingenico\.com\/v1\/#{@gateway.options[:merchant_id]}/, endpoint) + assert_match(/api\.preprod\.connect\.worldline-solutions\.com\/v1\/#{@gateway.options[:merchant_id]}/, endpoint) end.respond_with(successful_authorize_response) end @@ -92,17 +88,23 @@ def test_successful_purchase_with_requires_approval_true end.respond_with(successful_authorize_response, successful_capture_response) end - def test_purchase_request_with_google_pay + def test_purchase_request_with_encrypted_google_pay + google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :google_pay, + payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}" + }) + stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@accepted_amount, @google_pay_network_token) + @gateway.purchase(@accepted_amount, google_pay, { use_encrypted_payment_data: true }) end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| assert_equal '320', JSON.parse(data)['mobilePaymentMethodSpecificInput']['paymentProductId'] + assert_equal google_pay.payment_data, JSON.parse(data)['mobilePaymentMethodSpecificInput']['encryptedPaymentData'] end end - def test_purchase_request_with_google_pay_pan_only + def test_purchase_request_with_google_pay stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@accepted_amount, @google_pay_pan_only, @options.merge(customer: 'GP1234ID', google_pay_pan_only: true)) + @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 @@ -129,26 +131,7 @@ def test_add_payment_for_google_pay 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'] + assert_equal post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'], @google_pay_network_token.payment_data end def test_add_payment_for_apple_pay @@ -166,47 +149,6 @@ def test_add_payment_for_apple_pay 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) @@ -231,6 +173,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', @@ -379,9 +322,9 @@ def test_successful_authorization_with_extra_options response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, options) 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("fraudFields":{"website":"www.example.com","giftMessage":"Happy Day!"}), 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) @@ -392,7 +335,8 @@ def test_successful_authorize_with_3ds_auth response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@accepted_amount, @credit_card, @options_3ds2) end.check_request do |_method, _endpoint, data, _headers| - assert_match(/"threeDSecure\":{\"externalCardholderAuthenticationData\":{/, data) + assert_match(/threeDSecure/, data) + assert_match(/externalCardholderAuthenticationData/, data) assert_match(/"eci\":\"05\"/, data) assert_match(/"cavv\":\"jJ81HADVRtXfCBATEp01CJUAAAA=\"/, data) assert_match(/"xid\":\"BwABBJQ1AgAAAAAgJDUCAAAAAAA=\"/, data) @@ -424,6 +368,16 @@ def test_does_not_send_3ds_auth_when_empty assert_success response end + def test_successful_authorize_with_3ds_exemption + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@accepted_amount, @credit_card, { three_ds_exemption_type: 'moto' }) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"transactionChannel\":\"MOTO\"/, data) + end.respond_with(successful_authorize_with_3ds2_data_response) + + assert_success response + end + def test_truncates_first_name_to_15_chars credit_card = credit_card('4567350000427977', { first_name: 'thisisaverylongfirstname' }) @@ -447,7 +401,7 @@ def test_handles_blank_names assert_success response end - def test_truncates_address_fields + def test_truncates_split_address_fields response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, @credit_card, { billing_address: { @@ -460,7 +414,8 @@ def test_truncates_address_fields } }) end.check_request do |_method, _endpoint, data, _headers| - refute_match(/Supercalifragilisticexpialidociousthiscantbemorethanfiftycharacters/, data) + 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 diff --git a/test/unit/gateways/hi_pay_test.rb b/test/unit/gateways/hi_pay_test.rb new file mode 100644 index 00000000000..af35d72f1fe --- /dev/null +++ b/test/unit/gateways/hi_pay_test.rb @@ -0,0 +1,435 @@ +require 'test_helper' + +class HiPayTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = HiPayGateway.new(fixtures(:hi_pay)) + @credit_card = credit_card + @amount = 100 + + @options = { + order_id: SecureRandom.random_number(1000000000), + description: 'Short_description', + email: 'john.smith@test.com' + } + + @billing_address = address + end + + def test_tokenize_pm_with_authorize + @gateway.expects(:ssl_request). + with( + :post, + 'https://stage-secure2-vault.hipay-tpp.com/rest/v2/token/create', + all_of( + includes("card_number=#{@credit_card.number}"), + includes("card_expiry_month=#{@credit_card.month}"), + includes("card_expiry_year=#{@credit_card.year}"), + includes("card_holder=#{@credit_card.first_name}+#{@credit_card.last_name}"), + includes("cvc=#{@credit_card.verification_value}"), + includes('multi_use=0'), + includes('generate_request_id=0') + ), + anything + ). + returns(successful_tokenize_response) + @gateway.expects(:ssl_request).with(:post, 'https://stage-secure-gateway.hipay-tpp.com/rest/v1/order', anything, anything).returns(successful_authorize_response) + @gateway.authorize(@amount, @credit_card, @options) + end + + def test_tokenize_pm_with_store + @gateway.expects(:ssl_request). + with( + :post, + 'https://stage-secure2-vault.hipay-tpp.com/rest/v2/token/create', + all_of( + includes("card_number=#{@credit_card.number}"), + includes("card_expiry_month=#{@credit_card.month}"), + includes("card_expiry_year=#{@credit_card.year}"), + includes("card_holder=#{@credit_card.first_name}+#{@credit_card.last_name}"), + includes("cvc=#{@credit_card.verification_value}"), + includes('multi_use=1'), + includes('generate_request_id=0') + ), + anything + ). + returns(successful_tokenize_response) + @gateway.store(@credit_card, @options) + end + + def test_authorize_with_credit_card + @gateway.expects(:ssl_request). + with( + :post, + 'https://stage-secure2-vault.hipay-tpp.com/rest/v2/token/create', + all_of( + includes("card_number=#{@credit_card.number}"), + includes("card_expiry_month=#{@credit_card.month}"), + includes("card_expiry_year=#{@credit_card.year}"), + includes("card_holder=#{@credit_card.first_name}+#{@credit_card.last_name}"), + includes("cvc=#{@credit_card.verification_value}"), + includes('multi_use=0'), + includes('generate_request_id=0') + ), + anything + ). + returns(successful_tokenize_response) + + tokenize_response_token = JSON.parse(successful_tokenize_response)['token'] + + @gateway.expects(:ssl_request). + with( + :post, + 'https://stage-secure-gateway.hipay-tpp.com/rest/v1/order', + all_of( + includes('payment_product=visa'), + includes('operation=Authorization'), + regexp_matches(%r{orderid=\d+}), + includes("description=#{@options[:description]}"), + includes('currency=EUR'), + includes('amount=1.00'), + includes("cardtoken=#{tokenize_response_token}") + ), + anything + ). + returns(successful_capture_response) + + @gateway.authorize(@amount, @credit_card, @options) + end + + def test_authorize_with_credit_card_and_billing_address + @gateway.expects(:ssl_request).returns(successful_tokenize_response) + + tokenize_response_token = JSON.parse(successful_tokenize_response)['token'] + + @gateway.expects(:ssl_request). + with( + :post, + 'https://stage-secure-gateway.hipay-tpp.com/rest/v1/order', + all_of( + includes('payment_product=visa'), + includes('operation=Authorization'), + includes('streetaddress=456+My+Street'), + includes('streetaddress2=Apt+1'), + includes('city=Ottawa'), + includes('recipient_info=Widgets+Inc'), + includes('state=ON'), + includes('country=CA'), + includes('zipcode=K1C2N6'), + includes('phone=%28555%29555-5555'), + regexp_matches(%r{orderid=\d+}), + includes("description=#{@options[:description]}"), + includes('currency=EUR'), + includes('amount=1.00'), + includes("cardtoken=#{tokenize_response_token}") + ), + anything + ). + returns(successful_capture_response) + + @gateway.authorize(@amount, @credit_card, @options.merge({ billing_address: @billing_address })) + end + + def test_successfull_brand_mapping_mastercard + stub_comms do + @gateway.purchase(@amount, 'authorization_value|card_token|master', @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match(/payment_product=mastercard/, data) + end + end + + def test_purchase_with_stored_pm + stub_comms do + @gateway.purchase(@amount, 'authorization_value|card_token|card_brand', @options) + end.check_request do |_endpoint, data, _headers| + params = data.split('&').map { |param| param.split('=') }.to_h + assert_equal 'card_brand', params['payment_product'] + assert_equal 'Sale', params['operation'] + assert_equal @options[:order_id].to_s, params['orderid'] + assert_equal @options[:description], params['description'] + assert_equal 'EUR', params['currency'] + assert_equal '1.00', params['amount'] + assert_equal 'card_token', params['cardtoken'] + end.respond_with(successful_capture_response) + end + + def test_authorization_string_with_nil_values + auth_string_nil_value_first = @gateway.send :authorization_string, [nil, '123456', 'visa'] + assert_equal '123456|visa', auth_string_nil_value_first + + auth_string_nil_values = @gateway.send :authorization_string, [nil, 'token', nil] + assert_equal 'token', auth_string_nil_values + + auth_string_two_nil_values = @gateway.send :authorization_string, [nil, nil, 'visa'] + assert_equal 'visa', auth_string_two_nil_values + + auth_string_nil_values = @gateway.send :authorization_string, ['reference', nil, nil] + assert_equal 'reference', auth_string_nil_values + + auth_string_nil_values = @gateway.send :authorization_string, [nil, nil, nil] + assert_equal '', auth_string_nil_values + end + + def test_authorization_string_with_full_values + complete_auth_string = @gateway.send :authorization_string, %w(86786788 123456 visa) + assert_equal '86786788|123456|visa', complete_auth_string + end + + def test_purhcase_with_credit_card; end + + def test_capture + @gateway.expects(:ssl_request).with(:post, 'https://stage-secure2-vault.hipay-tpp.com/rest/v2/token/create', anything, anything).returns(successful_tokenize_response) + @gateway.expects(:ssl_request).with(:post, 'https://stage-secure-gateway.hipay-tpp.com/rest/v1/order', anything, anything).returns(successful_authorize_response) + + authorize_response = @gateway.authorize(@amount, @credit_card, @options) + transaction_reference, _card_token, _brand = authorize_response.authorization.split('|') + @gateway.expects(:ssl_request). + with( + :post, + "https://stage-secure-gateway.hipay-tpp.com/rest/v1/maintenance/transaction/#{transaction_reference}", + all_of( + includes('operation=capture'), + includes('currency=EUR'), + includes('amount=1.00') + ), + anything + ). + returns(successful_capture_response) + @gateway.capture(@amount, transaction_reference, @options) + end + + def test_refund + @gateway.expects(:ssl_request).with(:post, 'https://stage-secure2-vault.hipay-tpp.com/rest/v2/token/create', anything, anything).returns(successful_tokenize_response) + @gateway.expects(:ssl_request).with(:post, 'https://stage-secure-gateway.hipay-tpp.com/rest/v1/order', anything, anything).returns(successful_capture_response) + + authorize_response = @gateway.purchase(@amount, @credit_card, @options) + transaction_reference, _card_token, _brand = authorize_response.authorization.split('|') + @gateway.expects(:ssl_request). + with( + :post, + "https://stage-secure-gateway.hipay-tpp.com/rest/v1/maintenance/transaction/#{transaction_reference}", + all_of( + includes('operation=refund'), + includes('currency=EUR'), + includes('amount=1.00') + ), + anything + ). + returns(successful_refund_response) + @gateway.refund(@amount, transaction_reference, @options) + end + + def test_void + @gateway.expects(:ssl_request).with(:post, 'https://stage-secure2-vault.hipay-tpp.com/rest/v2/token/create', anything, anything).returns(successful_tokenize_response) + @gateway.expects(:ssl_request).with(:post, 'https://stage-secure-gateway.hipay-tpp.com/rest/v1/order', anything, anything).returns(successful_authorize_response) + + authorize_response = @gateway.authorize(@amount, @credit_card, @options) + transaction_reference, _card_token, _brand = authorize_response.authorization.split('|') + @gateway.expects(:ssl_request). + with( + :post, + "https://stage-secure-gateway.hipay-tpp.com/rest/v1/maintenance/transaction/#{transaction_reference}", + all_of( + includes('operation=cancel'), + includes('currency=EUR') + ), + anything + ). + returns(successful_void_response) + @gateway.void(transaction_reference, @options) + end + + def test_required_client_id_and_client_secret + error = assert_raises ArgumentError do + HiPayGateway.new + end + + assert_equal 'Missing required parameter: username', error.message + end + + def test_supported_card_types + assert_equal HiPayGateway.supported_cardtypes, %i[visa master american_express] + end + + def test_supported_countries + assert_equal HiPayGateway.supported_countries, ['FR'] + end + + # def test_support_scrubbing_flag_enabled + # assert @gateway.supports_scrubbing? + # end + + def test_detecting_successfull_response_from_capture + assert @gateway.send :success_from, 'capture', { 'status' => '118', 'message' => 'Captured' } + end + + def test_detecting_successfull_response_from_purchase + assert @gateway.send :success_from, 'order', { 'state' => 'completed' } + end + + def test_detecting_successfull_response_from_authorize + assert @gateway.send :success_from, 'order', { 'state' => 'completed' } + end + + def test_detecting_successfull_response_from_store + assert @gateway.send :success_from, 'store', { 'token' => 'random_token' } + end + + def test_get_response_message_from_messages_key + message = @gateway.send :message_from, 'order', { 'message' => 'hello' } + assert_equal 'hello', message + end + + def test_get_response_message_from_message_user + message = @gateway.send :message_from, 'order', { other_key: 'something_else' } + assert_nil message + end + + def test_url_generation_from_action + action = 'test' + assert_equal "#{@gateway.test_url}/v1/#{action}", @gateway.send(:url, action) + end + + def test_request_headers_building + gateway = HiPayGateway.new(username: 'abc123', password: 'def456') + headers = gateway.send :request_headers + + assert_equal 'application/json', headers['Accept'] + assert_equal 'application/x-www-form-urlencoded', headers['Content-Type'] + assert_equal 'Basic YWJjMTIzOmRlZjQ1Ng==', headers['Authorization'] + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def successful_tokenize_response + '{"token":"5fc03718289f58d1ce38482faa79aa4c640c44a5d182ad3d849761ed9ea33155","request_id":"0","card_id":"9fd81707-8f41-4a01-b6ed-279954336ada","multi_use":0,"brand":"VISA","pan":"411111xxxxxx1111","card_holder":"John Smith","card_expiry_month":"12","card_expiry_year":"2025","issuer":"JPMORGAN CHASE BANK, N.A.","country":"US","card_type":"CREDIT","forbidden_issuer_country":false}' + end + + def successful_authorize_response + '{"state":"completed","reason":"","forwardUrl":"","test":"true","mid":"00001331069","attemptId":"1","authorizationCode":"no_code","transactionReference":"800271033524","dateCreated":"2023-12-05T23:36:43+0000","dateUpdated":"2023-12-05T23:36:48+0000","dateAuthorized":"2023-12-05T23:36:48+0000","status":"116","message":"Authorized","authorizedAmount":"500.00","capturedAmount":"0.00","refundedAmount":"0.00","creditedAmount":"0.00","decimals":"2","currency":"EUR","ipAddress":"0.0.0.0","ipCountry":"","deviceId":"","cdata1":"","cdata2":"","cdata3":"","cdata4":"","cdata5":"","cdata6":"","cdata7":"","cdata8":"","cdata9":"","cdata10":"","avsResult":"","eci":"7","paymentProduct":"visa","paymentMethod":{"token":"5fc03718289f58d1ce38482faa79aa4c640c44a5d182ad3d849761ed9ea33155","cardId":"9fd81707-8f41-4a01-b6ed-279954336ada","brand":"VISA","pan":"411111******1111","cardHolder":"JOHN SMITH","cardExpiryMonth":"12","cardExpiryYear":"2025","issuer":"JPMORGAN CHASE BANK, N.A.","country":"US"},"threeDSecure":{"eci":"","authenticationStatus":"Y","authenticationMessage":"Authentication Successful","authenticationToken":"","xid":""},"fraudScreening":{"scoring":"0","result":"ACCEPTED","review":""},"order":{"id":"Sp_ORDER_272437225","dateCreated":"2023-12-05T23:36:43+0000","attempts":"1","amount":"500.00","shipping":"0.00","tax":"0.00","decimals":"2","currency":"EUR","customerId":"","language":"en_US","email":""},"debitAgreement":{"id":"","status":""}}' + end + + def successful_capture_response + '{"operation":"capture","test":"true","mid":"00001331069","authorizationCode":"no_code","transactionReference":"800271033524","dateCreated":"2023-12-05T23:36:43+0000","dateUpdated":"2023-12-05T23:37:21+0000","dateAuthorized":"2023-12-05T23:36:48+0000","status":"118","message":"Captured","authorizedAmount":"500.00","capturedAmount":"500.00","refundedAmount":"0.00","decimals":"2","currency":"EUR"}' + end + + def successful_refund_response + '{"operation":"refund","test":"true","mid":"00001331069","authorizationCode":"no_code","transactionReference":"800272279241","dateCreated":"2023-12-12T16:36:46+0000","dateUpdated":"2023-12-12T16:36:54+0000","dateAuthorized":"2023-12-12T16:36:50+0000","status":"124","message":"Refund Requested","authorizedAmount":"500.00","capturedAmount":"500.00","refundedAmount":"500.00","decimals":"2","currency":"EUR"}' + end + + def successful_void_response + '{"operation":"cancel","test":"true","mid":"00001331069","authorizationCode":"no_code","transactionReference":"800272279254","dateCreated":"2023-12-12T16:38:49+0000","dateUpdated":"2023-12-12T16:38:55+0000","dateAuthorized":"2023-12-12T16:38:53+0000","status":"175","message":"Authorization Cancellation requested","authorizedAmount":"500.00","capturedAmount":"0.00","refundedAmount":"0.00","decimals":"2","currency":"EUR"}' + end + + def pre_scrubbed + <<~PRE_SCRUBBED + opening connection to stage-secure2-vault.hipay-tpp.com:443... + opened + starting SSL for stage-secure2-vault.hipay-tpp.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- "POST /rest/v2/token/create HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nAuthorization: Basic OTQ2NTgzNjUuc3RhZ2Utc2VjdXJlLWdhdGV3YXkuaGlwYXktdHBwLmNvbTpUZXN0X1JoeXBWdktpUDY4VzNLQUJ4eUdoS3Zlcw==\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: stage-secure2-vault.hipay-tpp.com\r\nContent-Length: 136\r\n\r\n" + <- "card_number=4111111111111111&card_expiry_month=12&card_expiry_year=2025&card_holder=John+Smith&cvc=514&multi_use=0&generate_request_id=0" + -> "HTTP/1.1 201 Created\r\n" + -> "Server: nginx\r\n" + -> "Date: Tue, 12 Dec 2023 14:49:44 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Vary: Authorization\r\n" + -> "Cache-Control: max-age=0, must-revalidate, private\r\n" + -> "Expires: Tue, 12 Dec 2023 14:49:44 GMT\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "Set-Cookie: PHPSESSID=j9bfv7gaml9uslij70e15kvrm6; path=/; HttpOnly\r\n" + -> "Strict-Transport-Security: max-age=86400\r\n" + -> "\r\n" + -> "17c\r\n" + reading 380 bytes... + -> "{\"token\":\"0acbbfcbd5bf202a05acc0e9c00f79158a2fe8b60caad2213b09e901b89dc28e\",\"request_id\":\"0\",\"card_id\":\"9fd81707-8f41-4a01-b6ed-279954336ada\",\"multi_use\":0,\"brand\":\"VISA\",\"pan\":\"411111xxxxxx1111\",\"card_holder\":\"John Smith\",\"card_expiry_month\":\"12\",\"card_expiry_year\":\"2025\",\"issuer\":\"JPMORGAN CHASE BANK, N.A.\",\"country\":\"US\",\"card_type\":\"CREDIT\",\"forbidden_issuer_country\":false}" + reading 2 bytes... + -> "\r\n" + 0 + \r\nConn close + opening connection to stage-secure-gateway.hipay-tpp.com:443... + opened + starting SSL for stage-secure-gateway.hipay-tpp.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- "POST /rest/v1/order HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nAuthorization: Basic OTQ2NTgzNjUuc3RhZ2Utc2VjdXJlLWdhdGV3YXkuaGlwYXktdHBwLmNvbTpUZXN0X1JoeXBWdktpUDY4VzNLQUJ4eUdoS3Zlcw==\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nHost: stage-secure-gateway.hipay-tpp.com\r\nContent-Length: 186\r\n\r\n" + <- "payment_product=visa&operation=Sale&cardtoken=0acbbfcbd5bf202a05acc0e9c00f79158a2fe8b60caad2213b09e901b89dc28e&order_id=Sp_ORDER_100432071&description=An+authorize¤cy=EUR&amount=500" + -> "HTTP/1.1 200 OK\r\n" + -> "date: Tue, 12 Dec 2023 14:49:45 GMT\r\n" + -> "expires: Thu, 19 Nov 1981 08:52:00 GMT\r\n" + -> "cache-control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\n" + -> "pragma: no-cache\r\n" + -> "access-control-allow-origin: \r\n" + -> "access-control-allow-headers: \r\n" + -> "access-control-allow-credentials: true\r\n" + -> "content-length: 1472\r\n" + -> "content-type: application/json; encoding=UTF-8\r\n" + -> "connection: close\r\n" + -> "\r\n" + reading 1472 bytes... + -> "{\"state\":\"completed\",\"reason\":\"\",\"forwardUrl\":\"\",\"test\":\"true\",\"mid\":\"00001331069\",\"attemptId\":\"1\",\"authorizationCode\":\"no_code\",\"transactionReference\":\"800272278410\",\"referenceToPay\":\"\",\"dateCreated\":\"2023-12-12T14:49:45+0000\",\"dateUpdated\":\"2023-12-12T14:49:50+0000\",\"dateAuthorized\":\"2023-12-12T14:49:49+0000\",\"status\":\"118\",\"message\":\"Captured\",\"authorizedAmount\":\"500.00\",\"capturedAmount\":\"500.00\",\"refundedAmount\":\"0.00\",\"creditedAmount\":\"0.00\",\"decimals\":\"2\",\"currency\":\"EUR\",\"ipAddress\":\"0.0.0.0\",\"ipCountry\":\"\",\"deviceId\":\"\",\"cdata1\":\"\",\"cdata2\":\"\",\"cdata3\":\"\",\"cdata4\":\"\",\"cdata5\":\"\",\"cdata6\":\"\",\"cdata7\":\"\",\"cdata8\":\"\",\"cdata9\":\"\",\"cdata10\":\"\",\"avsResult\":\"\",\"eci\":\"7\",\"paymentProduct\":\"visa\",\"paymentMethod\":{\"token\":\"0acbbfcbd5bf202a05acc0e9c00f79158a2fe8b60caad2213b09e901b89dc28e\",\"cardId\":\"9fd81707-8f41-4a01-b6ed-279954336ada\",\"brand\":\"VISA\",\"pan\":\"411111******1111\",\"cardHolder\":\"JOHN SMITH\",\"cardExpiryMonth\":\"12\",\"cardExpiryYear\":\"2025\",\"issuer\":\"JPMORGAN CHASE BANK, N.A.\",\"country\":\"US\"},\"threeDSecure\":{\"eci\":\"\",\"authenticationStatus\":\"Y\",\"authenticationMessage\":\"Authentication Successful\",\"authenticationToken\":\"\",\"xid\":\"\"},\"fraudScreening\":{\"scoring\":\"0\",\"result\":\"ACCEPTED\",\"review\":\"\"},\"order\":{\"id\":\"Sp_ORDER_100432071\",\"dateCreated\":\"2023-12-12T14:49:45+0000\",\"attempts\":\"1\",\"amount\":\"500.00\",\"shipping\":\"0.00\",\"tax\":\"0.00\",\"decimals\":\"2\",\"currency\":\"EUR\",\"customerId\":\"\",\"language\":\"en_US\",\"email\":\"\"},\"debitAgreement\":{\"id\":\"\",\"status\":\"\"}}" + reading 1472 bytes... + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<~POST_SCRUBBED + opening connection to stage-secure2-vault.hipay-tpp.com:443... + opened + starting SSL for stage-secure2-vault.hipay-tpp.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- "POST /rest/v2/token/create HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nAuthorization: Basic [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: stage-secure2-vault.hipay-tpp.com\r\nContent-Length: 136\r\n\r\n" + <- "card_number=[FILTERED]&card_expiry_month=12&card_expiry_year=2025&card_holder=John+Smith&cvc=[FILTERED]&multi_use=0&generate_request_id=0" + -> "HTTP/1.1 201 Created\r\n" + -> "Server: nginx\r\n" + -> "Date: Tue, 12 Dec 2023 14:49:44 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Transfer-Encoding: chunked\r\n" + -> "Connection: close\r\n" + -> "Vary: Authorization\r\n" + -> "Cache-Control: max-age=0, must-revalidate, private\r\n" + -> "Expires: Tue, 12 Dec 2023 14:49:44 GMT\r\n" + -> "X-XSS-Protection: 1; mode=block\r\n" + -> "Set-Cookie: PHPSESSID=j9bfv7gaml9uslij70e15kvrm6; path=/; HttpOnly\r\n" + -> "Strict-Transport-Security: max-age=86400\r\n" + -> "\r\n" + -> "17c\r\n" + reading 380 bytes... + -> "{\"token\":\"0acbbfcbd5bf202a05acc0e9c00f79158a2fe8b60caad2213b09e901b89dc28e\",\"request_id\":\"0\",\"card_id\":\"9fd81707-8f41-4a01-b6ed-279954336ada\",\"multi_use\":0,\"brand\":\"VISA\",\"pan\":\"411111xxxxxx1111\",\"card_holder\":\"John Smith\",\"card_expiry_month\":\"12\",\"card_expiry_year\":\"2025\",\"issuer\":\"JPMORGAN CHASE BANK, N.A.\",\"country\":\"US\",\"card_type\":\"CREDIT\",\"forbidden_issuer_country\":false}" + reading 2 bytes... + -> "\r\n" + 0 + \r\nConn close + opening connection to stage-secure-gateway.hipay-tpp.com:443... + opened + starting SSL for stage-secure-gateway.hipay-tpp.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- "POST /rest/v1/order HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nAuthorization: Basic [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: stage-secure-gateway.hipay-tpp.com\r\nContent-Length: 186\r\n\r\n" + <- "payment_product=visa&operation=Sale&cardtoken=0acbbfcbd5bf202a05acc0e9c00f79158a2fe8b60caad2213b09e901b89dc28e&order_id=Sp_ORDER_100432071&description=An+authorize¤cy=EUR&amount=500" + -> "HTTP/1.1 200 OK\r\n" + -> "date: Tue, 12 Dec 2023 14:49:45 GMT\r\n" + -> "expires: Thu, 19 Nov 1981 08:52:00 GMT\r\n" + -> "cache-control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\n" + -> "pragma: no-cache\r\n" + -> "access-control-allow-origin: \r\n" + -> "access-control-allow-headers: \r\n" + -> "access-control-allow-credentials: true\r\n" + -> "content-length: 1472\r\n" + -> "content-type: application/json; encoding=UTF-8\r\n" + -> "connection: close\r\n" + -> "\r\n" + reading 1472 bytes... + -> "{\"state\":\"completed\",\"reason\":\"\",\"forwardUrl\":\"\",\"test\":\"true\",\"mid\":\"00001331069\",\"attemptId\":\"1\",\"authorizationCode\":\"no_code\",\"transactionReference\":\"800272278410\",\"referenceToPay\":\"\",\"dateCreated\":\"2023-12-12T14:49:45+0000\",\"dateUpdated\":\"2023-12-12T14:49:50+0000\",\"dateAuthorized\":\"2023-12-12T14:49:49+0000\",\"status\":\"118\",\"message\":\"Captured\",\"authorizedAmount\":\"500.00\",\"capturedAmount\":\"500.00\",\"refundedAmount\":\"0.00\",\"creditedAmount\":\"0.00\",\"decimals\":\"2\",\"currency\":\"EUR\",\"ipAddress\":\"0.0.0.0\",\"ipCountry\":\"\",\"deviceId\":\"\",\"cdata1\":\"\",\"cdata2\":\"\",\"cdata3\":\"\",\"cdata4\":\"\",\"cdata5\":\"\",\"cdata6\":\"\",\"cdata7\":\"\",\"cdata8\":\"\",\"cdata9\":\"\",\"cdata10\":\"\",\"avsResult\":\"\",\"eci\":\"7\",\"paymentProduct\":\"visa\",\"paymentMethod\":{\"token\":\"0acbbfcbd5bf202a05acc0e9c00f79158a2fe8b60caad2213b09e901b89dc28e\",\"cardId\":\"9fd81707-8f41-4a01-b6ed-279954336ada\",\"brand\":\"VISA\",\"pan\":\"411111******1111\",\"cardHolder\":\"JOHN SMITH\",\"cardExpiryMonth\":\"12\",\"cardExpiryYear\":\"2025\",\"issuer\":\"JPMORGAN CHASE BANK, N.A.\",\"country\":\"US\"},\"threeDSecure\":{\"eci\":\"\",\"authenticationStatus\":\"Y\",\"authenticationMessage\":\"Authentication Successful\",\"authenticationToken\":\"\",\"xid\":\"\"},\"fraudScreening\":{\"scoring\":\"0\",\"result\":\"ACCEPTED\",\"review\":\"\"},\"order\":{\"id\":\"Sp_ORDER_100432071\",\"dateCreated\":\"2023-12-12T14:49:45+0000\",\"attempts\":\"1\",\"amount\":\"500.00\",\"shipping\":\"0.00\",\"tax\":\"0.00\",\"decimals\":\"2\",\"currency\":\"EUR\",\"customerId\":\"\",\"language\":\"en_US\",\"email\":\"\"},\"debitAgreement\":{\"id\":\"\",\"status\":\"\"}}" + reading 1472 bytes... + Conn close + POST_SCRUBBED + end +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 d92cea9daa0..f2c8f658969 100644 --- a/test/unit/gateways/ipg_test.rb +++ b/test/unit/gateways/ipg_test.rb @@ -5,6 +5,7 @@ class IpgTest < Test::Unit::TestCase def setup @gateway = IpgGateway.new(fixtures(:ipg)) + @gateway_ma = IpgGateway.new(fixtures(:ipg_ma)) @credit_card = credit_card @amount = 100 @@ -19,6 +20,7 @@ def test_successful_purchase end.check_request do |_endpoint, data, _headers| doc = REXML::Document.new(data) assert_match('sale', REXML::XPath.first(doc, '//v1:CreditCardTxType//v1:Type').text) + assert_match('1.00', REXML::XPath.first(doc, '//v1:Transaction//v1:ChargeTotal').text) end.respond_with(successful_purchase_response) assert_success response @@ -129,6 +131,31 @@ def test_successful_purchase_with_store_id assert_success response end + def test_successful_ma_purchase_with_store_id + response = stub_comms(@gateway_ma) do + @gateway_ma.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_basic_auth_builds_correctly_with_differing_ma_credential_structures + user_id_without_ws = fixtures(:ipg_ma)[:user_id].sub(/^WS/, '') + gateway_ma2 = IpgGateway.new(fixtures(:ipg_ma).merge({ user_id: user_id_without_ws })) + + assert_equal(@gateway_ma.send(:build_header), gateway_ma2.send(:build_header)) + end + + def test_basic_auth_builds_correctly_with_differing_credential_structures + user_id_without_ws = fixtures(:ipg)[:user_id].sub(/^WS/, '') + gateway2 = IpgGateway.new(fixtures(:ipg).merge({ user_id: user_id_without_ws })) + + assert_equal(@gateway.send(:build_header), gateway2.send(:build_header)) + end + def test_successful_purchase_with_payment_token payment_token = 'ABC123' @@ -173,7 +200,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 @@ -194,7 +221,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 @@ -215,7 +242,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 @@ -236,7 +263,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 @@ -257,7 +284,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 @@ -332,6 +359,23 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed 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 + + def test_failed_without_store_id + bad_gateway = IpgGateway.new(fixtures(:ipg).merge({ store_id: nil })) + assert_raises(ArgumentError) do + bad_gateway.purchase(@amount, @credit_card, @options) + end + end + private def successful_purchase_response @@ -728,7 +772,7 @@ def post_scrubbed starting SSL for test.ipg-online.com:443... SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 <- "POST /ipgapi/services HTTP/1.1\r\nContent-Type: text/xml; charset=utf-8\r\nAuthorization: Basic [FILTERED]\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: test.ipg-online.com\r\nContent-Length: 850\r\n\r\n" - <- "\n \n \n \n \n \n [FILTERED]\n sale\n \n\n [FILTERED]\n 12\n 22\n [FILTERED]\n\n\n 100\n 032\n\n\n\n \n \n \n\n" + <- "\n \n \n \n \n \n 5921102002\n sale\n \n\n [FILTERED]\n 12\n 22\n [FILTERED]\n\n\n 100\n 032\n\n\n\n \n \n \n\n" -> "HTTP/1.1 200 \r\n" -> "Date: Fri, 29 Oct 2021 19:31:23 GMT\r\n" -> "Strict-Transport-Security: max-age=63072000; includeSubdomains\r\n" diff --git a/test/unit/gateways/kushki_test.rb b/test/unit/gateways/kushki_test.rb index b13d04653f5..8f104d53065 100644 --- a/test/unit/gateways/kushki_test.rb +++ b/test/unit/gateways/kushki_test.rb @@ -41,7 +41,27 @@ 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, + product_details: [ + { + id: 'test1', + title: 'tester1', + price: 10, + sku: 'abcde', + quantity: 1 + }, + { + id: 'test2', + title: 'tester2', + price: 5, + sku: 'edcba', + quantity: 2 + } + ] } amount = 100 * ( @@ -58,6 +78,9 @@ 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' + assert_includes data, 'productDetails' end.respond_with(successful_token_response, successful_charge_response) assert_success response @@ -272,6 +295,49 @@ def test_failed_refund assert_equal 'K010', refund.error_code end + def test_partial_refund + @gateway.expects(:ssl_post).returns(successful_charge_response) + @gateway.expects(:ssl_post).returns(successful_token_response) + + options = { currency: 'PEN' } + + purchase = @gateway.purchase(100, @credit_card, options) + + refund = stub_comms(@gateway, :ssl_request) do + refund_options = { + currency: 'PEN', + partial_refund: true, + full_response: true + } + @gateway.refund(50, purchase.authorization, refund_options) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['amount']['subtotalIva0'], 0.5 + end.respond_with(successful_refund_response) + assert_success refund + end + + def test_full_refund_does_not_have_request_body + @gateway.expects(:ssl_post).returns(successful_charge_response) + @gateway.expects(:ssl_post).returns(successful_token_response) + + options = { currency: 'PEN' } + + purchase = @gateway.purchase(@amount, @credit_card, options) + assert_success purchase + + refund = stub_comms(@gateway, :ssl_request) do + refund_options = { + currency: 'PEN', + full_response: true + } + @gateway.refund(@amount, purchase.authorization, refund_options) + end.check_request do |_method, _endpoint, data, _headers| + assert_nil(data) + end.respond_with(successful_refund_response) + assert_success refund + end + def test_successful_capture @gateway.expects(:ssl_post).returns(successful_authorize_response) @gateway.expects(:ssl_post).returns(successful_token_response) diff --git a/test/unit/gateways/litle_test.rb b/test/unit/gateways/litle_test.rb index 88b81d9d23c..4af53261b61 100644 --- a/test/unit/gateways/litle_test.rb +++ b/test/unit/gateways/litle_test.rb @@ -54,7 +54,8 @@ def setup name: 'John Smith', routing_number: '011075150', account_number: '1099999999', - account_type: 'checking' + account_type: nil, + account_holder_type: 'checking' ) @long_address = { @@ -76,7 +77,55 @@ 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_prepaid_card_141 + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_purchase_for_prepaid_cards_141) + + assert_success response + assert_equal 'Consumer non-reloadable prepaid card, Approved', response.message + assert_equal '141', response.params['response'] + end + + def test_successful_purchase_prepaid_card_142 + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_purchase_for_prepaid_cards_142) + + assert_success response + assert_equal 'Consumer single-use virtual card number, Approved', response.message + assert_equal '142', response.params['response'] + 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 @@ -104,6 +153,21 @@ def test_successful_postlive_url def test_successful_purchase_with_echeck response = stub_comms do @gateway.purchase(2004, @check) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(Checking), data) + end.respond_with(successful_purchase_with_echeck_response) + + assert_success response + + assert_equal '621100411297330000;echeckSales;2004', response.authorization + assert response.test? + end + + def test_successful_purchase_with_echeck_and_account_holder_type + response = stub_comms do + @gateway.purchase(2004, @authorize_check) + end.check_request do |_endpoint, data, _headers| + assert_match(%r(Checking), data) end.respond_with(successful_purchase_with_echeck_response) assert_success response @@ -569,7 +633,7 @@ def test_stored_credential_cit_card_on_file_used @gateway.authorize(@amount, @credit_card, options) end.check_request do |_endpoint, data, _headers| assert_match(%r(cardholderInitiatedCOF), data) - assert_match(%r(#{network_transaction_id}), data) + assert_not_match(%r(#{network_transaction_id}), data) assert_match(%r(ecommerce), data) end.respond_with(successful_authorize_stored_credentials) @@ -593,8 +657,8 @@ def test_stored_credential_cit_cof_doesnt_override_order_source @gateway.authorize(@amount, @credit_card, options) end.check_request do |_endpoint, data, _headers| assert_match(%r(cardholderInitiatedCOF), data) - assert_match(%r(#{network_transaction_id}), data) - assert_match(%r(3dsAuthenticated), data) + assert_not_match(%r(#{network_transaction_id}), data) + assert_match(%r(ecommerce), data) end.respond_with(successful_authorize_stored_credentials) assert_success response @@ -613,6 +677,7 @@ def test_stored_credential_mit_card_on_file_initial response = stub_comms do @gateway.authorize(@amount, @credit_card, options) end.check_request do |_endpoint, data, _headers| + assert_match(%r(ecommerce), data) assert_match(%r(initialCOF), data) end.respond_with(successful_authorize_stored_credentials) @@ -672,6 +737,7 @@ def test_stored_credential_installment_used response = stub_comms do @gateway.authorize(@amount, @credit_card, options) end.check_request do |_endpoint, data, _headers| + assert_not_match(%r(), data) assert_match(%r(#{network_transaction_id}), data) assert_match(%r(installment), data) end.respond_with(successful_authorize_stored_credentials) @@ -732,15 +798,15 @@ def network_transaction_id '63225578415568556365452427825' end - def successful_purchase_response + def successful_purchase_response(code = '000', message = 'Approved') %( 100000000000000006 1 - 000 + #{code} 2014-03-31T11:34:39 - Approved + #{message} 11111 01 @@ -784,6 +850,48 @@ def successful_purchase_with_echeck_response ) end + def successful_purchase_for_prepaid_cards_141 + %( + + + 456342657452 + 123456 + 141 + 2024-04-09T19:50:30 + 2024-04-09 + Consumer non-reloadable prepaid card, Approved + 382410 + + 01 + M + + MPMMPMPMPMPU + + + ) + end + + def successful_purchase_for_prepaid_cards_142 + %( + + + 456342657452 + 123456 + 142 + 2024-04-09T19:50:30 + 2024-04-09 + Consumer single-use virtual card number, Approved + 382410 + + 01 + M + + MPMMPMPMPMPU + + + ) + end + def successful_authorize_stored_credentials %( diff --git a/test/unit/gateways/mercado_pago_test.rb b/test/unit/gateways/mercado_pago_test.rb index 6af0e181c6d..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 = { diff --git a/test/unit/gateways/merchant_warrior_test.rb b/test/unit/gateways/merchant_warrior_test.rb index 7838382d861..b06fdb8320b 100644 --- a/test/unit/gateways/merchant_warrior_test.rb +++ b/test/unit/gateways/merchant_warrior_test.rb @@ -19,6 +19,14 @@ def setup address: address, transaction_product: 'TestProduct' } + @three_ds_secure = { + version: '2.2.0', + cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', + eci: '05', + xid: 'ODUzNTYzOTcwODU5NzY3Qw==', + enrolled: 'true', + authentication_response_status: 'Y' + } end def test_successful_authorize @@ -299,6 +307,34 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_three_ds_v2_object_construction + post = {} + @options[:three_d_secure] = @three_ds_secure + + @gateway.send(:add_three_ds, post, @options) + ds_options = @options[:three_d_secure] + + assert_equal ds_options[:version], post[:threeDSV2Version] + assert_equal ds_options[:cavv], post[:threeDSCavv] + assert_equal ds_options[:eci], post[:threeDSEci] + assert_equal ds_options[:xid], post[:threeDSXid] + assert_equal ds_options[:authentication_response_status], post[:threeDSStatus] + end + + def test_purchase_with_three_ds + @options[:three_d_secure] = @three_ds_secure + stub_comms(@gateway) do + @gateway.purchase(@success_amount, @credit_card, @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + params = URI.decode_www_form(data).to_h + assert_equal '2.2.0', params['threeDSV2Version'] + assert_equal '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', params['threeDSCavv'] + assert_equal '05', params['threeDSEci'] + assert_equal 'ODUzNTYzOTcwODU5NzY3Qw==', params['threeDSXid'] + assert_equal 'Y', params['threeDSStatus'] + end + end + private def successful_purchase_response 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/moneris_test.rb b/test/unit/gateways/moneris_test.rb index 5c7ee922fbc..feefeace8c6 100644 --- a/test/unit/gateways/moneris_test.rb +++ b/test/unit/gateways/moneris_test.rb @@ -36,35 +36,58 @@ 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_success response + assert_equal '69785-0_98;a131684dbecc1d89d9927c539ed3791b', response.authorization + end + + def test_successful_purchase_with_cust_id + response = stub_comms do + @gateway.purchase(100, @credit_card, @options.merge(cust_id: 'test1234')) + end.check_request do |_endpoint, data, _headers| + assert_match(/test1234<\/cust_id>/, 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 +151,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 +302,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/mundipagg_test.rb b/test/unit/gateways/mundipagg_test.rb index 7c9f4d923a8..a66d824333e 100644 --- a/test/unit/gateways/mundipagg_test.rb +++ b/test/unit/gateways/mundipagg_test.rb @@ -41,26 +41,26 @@ def setup @submerchant_options = { submerchant: { - "merchant_category_code": '44444', - "payment_facilitator_code": '5555555', - "code": 'code2', - "name": 'Sub Tony Stark', - "document": '123456789', - "type": 'individual', - "phone": { - "country_code": '55', - "number": '000000000', - "area_code": '21' + merchant_category_code: '44444', + payment_facilitator_code: '5555555', + code: 'code2', + name: 'Sub Tony Stark', + document: '123456789', + type: 'individual', + phone: { + country_code: '55', + number: '000000000', + area_code: '21' }, - "address": { - "street": 'Malibu Point', - "number": '10880', - "complement": 'A', - "neighborhood": 'Central Malibu', - "city": 'Malibu', - "state": 'CA', - "country": 'US', - "zip_code": '24210-460' + address: { + street: 'Malibu Point', + number: '10880', + complement: 'A', + neighborhood: 'Central Malibu', + city: 'Malibu', + state: 'CA', + country: 'US', + zip_code: '24210-460' } } } diff --git a/test/unit/gateways/nab_transact_test.rb b/test/unit/gateways/nab_transact_test.rb index b4afc7c7313..264d40dac66 100644 --- a/test/unit/gateways/nab_transact_test.rb +++ b/test/unit/gateways/nab_transact_test.rb @@ -228,9 +228,7 @@ def valid_metadata(name, location) end def assert_metadata(name, location, &block) - stub_comms(@gateway, :ssl_request) do - yield - end.check_request do |_method, _endpoint, data, _headers| + stub_comms(@gateway, :ssl_request, &block).check_request do |_method, _endpoint, data, _headers| metadata_matcher = Regexp.escape(valid_metadata(name, location)) assert_match %r{#{metadata_matcher}}, data end.respond_with(successful_purchase_response) diff --git a/test/unit/gateways/nmi_test.rb b/test/unit/gateways/nmi_test.rb index 7cbf9ef6510..f2817a6a38e 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 2, (%w[US CA] | NmiGateway.supported_countries).size end def test_supported_card_types @@ -594,6 +657,21 @@ def test_stored_credential_recurring_mit_used assert_success response end + def test_stored_credential_ntid_override_recurring_mit_used + options = stored_credential_options(:merchant, :recurring) + options[:network_transaction_id] = 'test123' + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/initiated_by=merchant/, data) + assert_match(/stored_credential_indicator=used/, data) + assert_match(/billing_method=recurring/, data) + assert_match(/initial_transaction_id=test123/, data) + end.respond_with(successful_authorization_response) + + assert_success response + end + def test_stored_credential_installment_cit_initial options = stored_credential_options(:cardholder, :installment, :initial) response = stub_comms do @@ -760,8 +838,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 906ea36c184..8adb208a32a 100644 --- a/test/unit/gateways/ogone_test.rb +++ b/test/unit/gateways/ogone_test.rb @@ -453,6 +453,27 @@ def test_transcript_scrubbing assert_equal @gateway.scrub(pre_scrub), post_scrub end + def test_signatire_calculation_with_with_space + payload = { + orderID: 'abc123', + currency: 'EUR', + amount: '100', + PM: 'CreditCard', + ACCEPTANCE: 'test123', + STATUS: '9', + CARDNO: 'XXXXXXXXXXXX3310', + ED: '1029', + DCC_INDICATOR: '0', + DCC_EXCHRATE: '' + } + + signature_with = @gateway.send(:calculate_signature, payload, 'sha512', 'ABC123') + payload.delete(:DCC_EXCHRATE) + signature_without = @gateway.send(:calculate_signature, payload, 'sha512', 'ABC123') + + assert_equal signature_without, signature_with + end + private def string_to_digest 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 0e4b306bc5b..150cc874ac1 100644 --- a/test/unit/gateways/orbital_test.rb +++ b/test/unit/gateways/orbital_test.rb @@ -30,7 +30,15 @@ def setup address2: address[:address2], city: address[:city], state: address[:state], - zip: address[:zip] + zip: address[:zip], + requestor_name: 'ArtVandelay123', + total_tax_amount: '75', + national_tax: '625', + pst_tax_reg_number: '8675309', + customer_vat_reg_number: '1234567890', + merchant_vat_reg_number: '987654321', + commodity_code: 'SUMM', + local_tax_rate: '6250' } @level3 = { @@ -42,7 +50,11 @@ def setup vat_tax: '25', alt_tax: '30', vat_rate: '7', - alt_ind: 'Y' + alt_ind: 'Y', + invoice_discount_treatment: 1, + tax_treatment: 1, + ship_vat_rate: 10, + unique_vat_invoice_ref: 'ABC123' } @line_items = @@ -123,7 +135,7 @@ def test_successful_purchase assert response = @gateway.purchase(50, credit_card, order_id: '1') assert_instance_of Response, response assert_success response - assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1', response.authorization + assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI', response.authorization end def test_successful_purchase_with_echeck @@ -133,7 +145,7 @@ def test_successful_purchase_with_echeck assert_instance_of Response, response assert_equal 'Approved', response.message assert_success response - assert_equal '5F8E8BEE7299FD339A38F70CFF6E5D010EF55498;9baedc697f2cf06457de78', response.authorization + assert_equal '5F8E8BEE7299FD339A38F70CFF6E5D010EF55498;9baedc697f2cf06457de78;EC', response.authorization end def test_successful_purchase_with_commercial_echeck @@ -143,6 +155,7 @@ def test_successful_purchase_with_commercial_echeck @gateway.purchase(50, commercial_echeck, order_id: '9baedc697f2cf06457de78') end.check_request do |_endpoint, data, _headers| assert_match %{X}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_with_echeck_response) end @@ -163,7 +176,7 @@ def test_successful_force_capture_with_echeck assert_instance_of Response, response assert_match 'APPROVAL', response.message assert_equal 'Approved and Completed', response.params['status_msg'] - assert_equal '5F8ED3D950A43BD63369845D5385B6354C3654B4;2930847bc732eb4e8102cf', response.authorization + assert_equal '5F8ED3D950A43BD63369845D5385B6354C3654B4;2930847bc732eb4e8102cf;EC', response.authorization end def test_successful_force_capture_with_echeck_prenote @@ -173,7 +186,7 @@ def test_successful_force_capture_with_echeck_prenote assert_instance_of Response, response assert_match 'APPROVAL', response.message assert_equal 'Approved and Completed', response.params['status_msg'] - assert_equal '5F8ED3D950A43BD63369845D5385B6354C3654B4;2930847bc732eb4e8102cf', response.authorization + assert_equal '5F8ED3D950A43BD63369845D5385B6354C3654B4;2930847bc732eb4e8102cf;EC', response.authorization end def test_failed_force_capture_with_echeck_prenote @@ -202,6 +215,15 @@ def test_level2_data assert_match %{#{@level2[:address2]}}, data assert_match %{#{@level2[:city]}}, data assert_match %{#{@level2[:state]}}, data + assert_match %{#{@level2[:requestor_name]}}, data + assert_match %{#{@level2[:total_tax_amount]}}, data + assert_match %{#{@level2[:national_tax]}}, data + assert_match %{#{@level2[:pst_tax_reg_number]}}, data + assert_match %{#{@level2[:customer_vat_reg_number]}}, data + assert_match %{#{@level2[:merchant_vat_reg_number]}}, data + assert_match %{#{@level2[:commodity_code]}}, data + assert_match %{#{@level2[:local_tax_rate]}}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) end @@ -218,6 +240,11 @@ def test_level3_data assert_match %{#{@level3[:vat_rate].to_i}}, data assert_match %{#{@level3[:alt_tax].to_i}}, data assert_match %{#{@level3[:alt_ind]}}, data + assert_match %{#{@level3[:invoice_discount_treatment]}}, data + assert_match %{#{@level3[:tax_treatment]}}, data + assert_match %{#{@level3[:ship_vat_rate]}}, data + assert_match %{#{@level3[:unique_vat_invoice_ref]}}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) end @@ -238,6 +265,7 @@ def test_line_items_data assert_match %{#{@line_items[1][:gross_net]}}, data assert_match %{#{@line_items[1][:disc_ind]}}, data assert_match %{2}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) end @@ -264,11 +292,15 @@ def test_network_tokenization_credit_card_data assert_match %{5}, data assert_match %{Y}, data assert_match %{DigitalTokenCryptogram}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) end def test_schema_for_soft_descriptors_with_network_tokenization_credit_card_data options = @options.merge( + level_2_data: @level2, + level_3_data: @level3, + line_items: @line_items, soft_descriptors: { merchant_name: 'Merch', product_description: 'Description', @@ -278,8 +310,7 @@ def test_schema_for_soft_descriptors_with_network_tokenization_credit_card_data stub_comms do @gateway.purchase(50, network_tokenization_credit_card(nil, eci: '5', transaction_id: 'BwABB4JRdgAAAAAAiFF2AAAAAAA='), options) end.check_request do |_endpoint, data, _headers| - # Soft descriptor fields should come before dpan and cryptogram fields - assert_match %{email@example<\/SDMerchantEmail>Y<\/DPANInd>5}, data assert_match %{TESTCAVV}, data assert_match %{TESTXID}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) end @@ -300,6 +332,7 @@ def test_three_d_secure_data_on_visa_authorization assert_match %{5}, data assert_match %{TESTCAVV}, data assert_match %{TESTXID}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) end @@ -312,6 +345,7 @@ def test_three_d_secure_data_on_master_purchase assert_match %{2}, data assert_match %{97267598FAE648F28083C23433990FBC}, data assert_match %{4}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) end @@ -324,6 +358,7 @@ def test_three_d_secure_data_on_master_authorization assert_match %{2}, data assert_match %{97267598FAE648F28083C23433990FBC}, data assert_match %{4}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) end @@ -348,6 +383,7 @@ def test_three_d_secure_data_on_master_sca_recurring assert_match %{97267598FAE648F28083C23433990FBC}, data assert_match %{Y}, data assert_match %{4}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) end @@ -509,9 +545,9 @@ def test_order_id_format def test_order_id_format_for_capture response = stub_comms do - @gateway.capture(101, '4A5398CF9B87744GG84A1D30F2F2321C66249416;1001.1', order_id: '#1001.1') + @gateway.capture(101, '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI001.1;VI', order_id: '#1001.1') end.check_request do |_endpoint, data, _headers| - assert_match(/1001-1<\/OrderID>/, data) + assert_match(/1<\/OrderID>/, data) end.respond_with(successful_purchase_response) assert_success response end @@ -524,9 +560,10 @@ def test_numeric_merchant_id_for_caputre ) response = stub_comms(gateway) do - gateway.capture(101, '4A5398CF9B87744GG84A1D30F2F2321C66249416;1', @options) + gateway.capture(101, '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI', @options) end.check_request do |_endpoint, data, _headers| assert_match(/700000123456<\/MerchantID>/, data) + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) assert_success response end @@ -558,9 +595,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) @@ -644,13 +679,13 @@ def test_address_format assert_match(/Luxury SuiteJoan Smith/, data) assert_match(/1234567890/, data) assert_match(/US/, data) + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) assert_success response @@ -777,9 +810,7 @@ def test_name_sends_for_credit_card_with_address dest_country: 'US' ) - 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, address: address) @@ -817,9 +848,7 @@ def test_name_sends_for_echeck_with_no_address end def test_does_not_send_for_credit_card_with_no_address - 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, address: nil, billing_address: nil) @@ -840,9 +869,7 @@ def test_avs_name_falls_back_to_billing_address country: 'US' ) - card = credit_card('4242424242424242', - first_name: nil, - last_name: '') + card = credit_card('4242424242424242', first_name: nil, last_name: '') response = stub_comms do @gateway.purchase(50, card, order_id: 1, billing_address: billing_address) @@ -863,9 +890,7 @@ def test_completely_blank_name country: 'US' ) - card = credit_card('4242424242424242', - first_name: nil, - last_name: nil) + card = credit_card('4242424242424242', first_name: nil, last_name: nil) response = stub_comms do @gateway.purchase(50, card, order_id: 1, billing_address: billing_address) @@ -881,6 +906,7 @@ def test_successful_purchase_with_negative_stored_credentials_indicator end.check_request do |_endpoint, data, _headers| assert_no_match(//, data) assert_no_match(//, data) + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) end @@ -891,6 +917,7 @@ def test_successful_purchase_with_stored_credentials assert_match %{#{@options_stored_credentials[:mit_msg_type]}}, data assert_match %{#{@options_stored_credentials[:mit_stored_credential_ind]}}, data assert_match %{#{@options_stored_credentials[:mit_submitted_transaction_id]}}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) end @@ -915,6 +942,7 @@ def test_stored_credential_recurring_cit_initial end.check_request do |_endpoint, data, _headers| assert_match(/CSTOYCRECYCSTOYMRECYabc123CSTOYCUSEYCSTOYMUSEYabc123CSTOYCINSYGT}, data + end + end + + def test_successful_payment_request_with_token_stored + stub_comms do + @gateway.purchase(50, '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;2521002395820006;VI', @options.merge(card_brand: 'VI')) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match %{VI}, data + assert_match %{2521002395820006}, data + end + end + + def test_successful_store + @gateway.expects(:ssl_post).returns(successful_store_response) + + assert response = @gateway.store(@credit_card, @options) + assert_instance_of Response, response + assert_equal 'Approved', response.message + assert_equal '4556761607723886', response.params['safetech_token'] + assert_equal 'VI', response.params['card_brand'] + end + + def test_successful_purchase_with_stored_token + @gateway.expects(:ssl_post).returns(successful_store_response) + assert store = @gateway.store(@credit_card, @options) + assert_instance_of Response, store + + @gateway.expects(:ssl_post).returns(successful_purchase_response) + assert auth = @gateway.purchase(100, store.authorization, @options) + assert_instance_of Response, auth + + assert_equal 'Approved', auth.message + end + def test_successful_purchase_with_overridden_normalized_stored_credentials stub_comms do @gateway.purchase(50, credit_card, @options.merge(@normalized_mit_stored_credential).merge(@options_stored_credentials)) @@ -1068,6 +1144,7 @@ def test_successful_purchase_with_overridden_normalized_stored_credentials assert_match %{MRSB}, data assert_match %{Y}, data assert_match %{123456abcdef}, data + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) end @@ -1083,6 +1160,7 @@ def test_default_managed_billing assert_match(/IO/, data) assert_match(/10102014/, data) assert_match(/N/, data) + assert_xml_valid_to_xsd(data) end.respond_with(successful_profile_response) assert_success response end @@ -1091,13 +1169,15 @@ def test_managed_billing response = stub_comms do assert_deprecation_warning(Gateway::RECURRING_DEPRECATION_MESSAGE) do assert_deprecation_warning do - @gateway.add_customer_profile(credit_card, + @gateway.add_customer_profile( + credit_card, managed_billing: { start_date: '10-10-2014', end_date: '10-10-2015', max_dollar_value: 1500, max_transactions: 12 - }) + } + ) end end end.check_request do |_endpoint, data, _headers| @@ -1214,7 +1294,7 @@ def test_successful_authorize_with_echeck assert_instance_of Response, response assert_equal 'Approved', response.message assert_success response - assert_equal '5F8E8D2B077217F3EF1ACD3B61610E4CD12954A3;2', response.authorization + assert_equal '5F8E8D2B077217F3EF1ACD3B61610E4CD12954A3;2;EC', response.authorization end def test_failed_authorize_with_echeck @@ -1359,10 +1439,7 @@ def test_american_requests_adhere_to_xml_schema response = stub_comms do @gateway.purchase(50, credit_card, order_id: 1, billing_address: address) end.check_request do |_endpoint, data, _headers| - schema_file = File.read("#{File.dirname(__FILE__)}/../../schema/orbital/Request_PTI83.xsd") - doc = Nokogiri::XML(data) - xsd = Nokogiri::XML::Schema(schema_file) - assert xsd.valid?(doc), 'Request does not adhere to DTD' + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) assert_success response end @@ -1371,10 +1448,7 @@ def test_german_requests_adhere_to_xml_schema response = stub_comms do @gateway.purchase(50, credit_card, order_id: 1, billing_address: address(country: 'DE')) end.check_request do |_endpoint, data, _headers| - schema_file = File.read("#{File.dirname(__FILE__)}/../../schema/orbital/Request_PTI83.xsd") - doc = Nokogiri::XML(data) - xsd = Nokogiri::XML::Schema(schema_file) - assert xsd.valid?(doc), 'Request does not adhere to DTD' + assert_xml_valid_to_xsd(data) end.respond_with(successful_purchase_response) assert_success response end @@ -1545,8 +1619,7 @@ def test_cc_account_num_is_removed_from_response response = nil assert_deprecation_warning do - response = @gateway.add_customer_profile(credit_card, - billing_address: address) + response = @gateway.add_customer_profile(credit_card, billing_address: address) end assert_instance_of Response, response @@ -1559,7 +1632,7 @@ def test_successful_verify @gateway.verify(credit_card, @options) end.respond_with(successful_purchase_response, successful_purchase_response) assert_success response - assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1', response.authorization + assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI', response.authorization assert_equal 'Approved', response.message end @@ -1587,7 +1660,7 @@ def test_successful_verify_zero_auth_different_cards @gateway.verify(@credit_card, @options) end.respond_with(successful_purchase_response) assert_success response - assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1', response.authorization + assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI', response.authorization assert_equal 'Approved', response.message end @@ -1606,7 +1679,7 @@ def test_successful_verify_with_discover_brand @gateway.verify(@credit_card, @options) end.respond_with(successful_purchase_response, successful_void_response) assert_success response - assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1', response.authorization + assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI', response.authorization assert_equal 'Approved', response.message end @@ -1616,7 +1689,7 @@ def test_successful_verify_and_failed_void_discover_brand @gateway.verify(credit_card, @options) end.respond_with(successful_purchase_response, failed_purchase_response) assert_success response - assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1', response.authorization + assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI', response.authorization assert_equal 'Approved', response.message end @@ -1625,7 +1698,7 @@ def test_successful_verify_and_failed_void @gateway.verify(credit_card, @options) end.respond_with(successful_purchase_response, failed_purchase_response) assert_success response - assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1', response.authorization + assert_equal '4A5398CF9B87744GG84A1D30F2F2321C66249416;1;VI', response.authorization assert_equal 'Approved', response.message end @@ -1770,6 +1843,10 @@ def successful_refund_response 'R253997001VI45567610299838860c1792db5d167e0b96dd9c60D1E12322FD50E1517A2598593A48EEA9965469201003 tst743Approved100 090955' end + def successful_store_response + 'AC492310001VI4556761029983886f9269cbc7eb453d75adb1d6536A0990C37C45D0000082B0001A64E4156534A101007 Mtst443Approved100IUM123433455676160772388600A' + end + def failed_refund_response '881The LIDM you supplied (3F3F3F) does not match with any existing transaction' end @@ -1923,4 +2000,14 @@ def post_scrubbed_echeck Conn close REQUEST end + + def assert_xml_valid_to_xsd(data) + doc = Nokogiri::XML(data) + xsd = Nokogiri::XML::Schema(schema_file) + assert xsd.valid?(doc), 'Request does not adhere to DTD' + end + + def schema_file + @schema_file ||= File.read("#{File.dirname(__FILE__)}/../../schema/orbital/Request_PTI95.xsd") + end end 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 09f13807e83..0f44fb59822 100644 --- a/test/unit/gateways/pay_trace_test.rb +++ b/test/unit/gateways/pay_trace_test.rb @@ -1,20 +1,10 @@ require 'test_helper' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - class PayTraceGateway < Gateway - def acquire_access_token - @options[:access_token] = SecureRandom.hex(16) - end - end - end -end - class PayTraceTest < Test::Unit::TestCase include CommStub def setup - @gateway = PayTraceGateway.new(username: 'username', password: 'password', integrator_id: 'uniqueintegrator') + @gateway = PayTraceGateway.new(username: 'username', password: 'password', integrator_id: 'uniqueintegrator', access_token: SecureRandom.hex(16)) @credit_card = credit_card @echeck = check(account_number: '123456', routing_number: '325070760') @amount = 100 @@ -24,17 +14,38 @@ def setup } end + def test_setup_access_token_should_rise_an_exception_under_bad_request + error = assert_raises(ActiveMerchant::OAuthResponseError) do + access_token_response = { + error: 'invalid_grant', + error_description: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' + }.to_json + @gateway.expects(:ssl_post).returns(access_token_response) + @gateway.send(:acquire_access_token) + end + + assert_match(/Failed with The provided authorization grant is invalid/, error.message) + end + def test_successful_purchase - @gateway.expects(:ssl_post).returns(successful_purchase_response) + response = stub_comms(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['amount'], '1.00' + assert_equal request['credit_card']['number'], @credit_card.number + assert_equal request['integrator_id'], @gateway.options[:integrator_id] + assert_equal request['csc'], @credit_card.verification_value + end.respond_with(successful_purchase_response) - response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal 392483066, response.authorization end def test_successful_purchase_with_ach + @echeck.name = 'Test Name' response = stub_comms(@gateway) do - @gateway.purchase(@amount, @echeck, @options) + @gateway.purchase(@amount, @echeck, {}) end.check_request do |endpoint, data, _headers| request = JSON.parse(data) assert_include endpoint, 'checks/sale/by_account' @@ -42,11 +53,8 @@ def test_successful_purchase_with_ach 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] + assert_equal request['billing_address']['name'], @echeck.name + assert_equal request.dig('billing_address', 'street_address'), nil end.respond_with(successful_ach_processing_response) assert_success response @@ -397,7 +405,7 @@ def pre_scrubbed starting SSL for api.paytrace.com:443... SSL established <- "POST /v1/transactions/sale/keyed HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Bearer 96e647567627164796f6e63704370727565646c697e236f6d6:5427e43707866415555426a68723848763574533d476a466:QryC8bI6hfidGVcFcwnago3t77BSzW8ItUl9GWhsx9Y\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.paytrace.com\r\nContent-Length: 335\r\n\r\n" - <- "{\"amount\":\"1.00\",\"credit_card\":{\"number\":\"4012000098765439\",\"expiration_month\":9,\"expiration_year\":2022},\"billing_address\":{\"name\":\"Longbob Longsen\",\"street_address\":\"456 My Street\",\"city\":\"Ottawa\",\"state\":\"ON\",\"zip\":\"K1C2N6\"},\"password\":\"ErNsphFQUEbjx2Hx6uT3MgJf\",\"username\":\"integrations@spreedly.com\",\"integrator_id\":\"9575315uXt4u\"}" + <- "{\"amount\":\"1.00\",\"credit_card\":{\"number\":\"4012000098765439\",\"expiration_month\":9,\"expiration_year\":2022},\"csc\":\"123\",\"billing_address\":{\"name\":\"Longbob Longsen\",\"street_address\":\"456 My Street\",\"city\":\"Ottawa\",\"state\":\"ON\",\"zip\":\"K1C2N6\"},\"password\":\"ErNsphFQUEbjx2Hx6uT3MgJf\",\"username\":\"integrations@spreedly.com\",\"integrator_id\":\"9575315uXt4u\"}" -> "HTTP/1.1 200 OK\r\n" -> "Date: Thu, 03 Jun 2021 22:03:24 GMT\r\n" -> "Content-Type: application/json; charset=utf-8\r\n" @@ -440,7 +448,7 @@ def post_scrubbed starting SSL for api.paytrace.com:443... SSL established <- "POST /v1/transactions/sale/keyed HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Bearer [FILTERED]\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.paytrace.com\r\nContent-Length: 335\r\n\r\n" - <- "{\"amount\":\"1.00\",\"credit_card\":{\"number\":\"[FILTERED]\",\"expiration_month\":9,\"expiration_year\":2022},\"billing_address\":{\"name\":\"Longbob Longsen\",\"street_address\":\"456 My Street\",\"city\":\"Ottawa\",\"state\":\"ON\",\"zip\":\"K1C2N6\"},\"password\":\"[FILTERED]\",\"username\":\"[FILTERED]\"}" + <- "{\"amount\":\"1.00\",\"credit_card\":{\"number\":\"[FILTERED]\",\"expiration_month\":9,\"expiration_year\":2022},\"csc\":\"[FILTERED]\",\"billing_address\":{\"name\":\"Longbob Longsen\",\"street_address\":\"456 My Street\",\"city\":\"Ottawa\",\"state\":\"ON\",\"zip\":\"K1C2N6\"},\"password\":\"[FILTERED]\",\"username\":\"[FILTERED]\"}" -> "HTTP/1.1 200 OK\r\n" -> "Date: Thu, 03 Jun 2021 22:03:24 GMT\r\n" -> "Content-Type: application/json; charset=utf-8\r\n" 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 22eab291105..53a7c090975 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' @@ -55,6 +55,16 @@ def setup 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 @@ -88,8 +98,15 @@ def test_invalid_token_on_integration end def test_successful_purchase - @gateway.expects(:ssl_post).returns(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options) + @credit_card.first_name = nil + @credit_card.last_name = nil + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal 'Jim Smith', request.dig('credit_card', 'cardholder_name') + end.respond_with(successful_purchase_response) assert_success response assert_equal 'ET114541|55083431|credit_card|1', response.authorization assert response.test? @@ -107,6 +124,29 @@ def test_successful_purchase_with_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 @@ -188,11 +228,34 @@ def test_successful_purchase_with_customer_ref assert_success response end + def test_successful_purchase_with_customer_ref_top_level + options = @options.merge(customer_ref: 'abcde') + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"customer_ref":"abcde"/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_reference_3 + options = @options.merge(reference_3: '12345') + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/"reference_3":"12345"/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + 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 @@ -204,7 +267,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 @@ -778,7 +872,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 @@ -863,7 +957,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 @@ -908,7 +1002,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 @@ -948,7 +1042,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 @@ -987,7 +1081,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 @@ -1012,7 +1106,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 @@ -1037,6 +1131,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..c3e303be2e2 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 = { @@ -28,8 +30,8 @@ def setup @eci = '01' @three_ds_v1_version = '1.0.2' @three_ds_v2_version = '2.1.0' - @three_ds_server_trans_id = 'three-ds-v2-trans-id' @authentication_response_status = 'Y' + @directory_server_transaction_id = 'directory_server_transaction_id' @three_ds_v1_mpi = { cavv: @cavv, @@ -42,8 +44,8 @@ def setup cavv: @cavv, eci: @eci, version: @three_ds_v2_version, - three_ds_server_trans_id: @three_ds_server_trans_id, - authentication_response_status: @authentication_response_status + authentication_response_status: @authentication_response_status, + ds_transaction_id: @directory_server_transaction_id } end @@ -57,6 +59,22 @@ def test_successful_purchase assert response.test? end + def test_rejected_purchase + @gateway.expects(:ssl_post).returns(purchase_rejected_status) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'Fondos Insuficientes', response.message + end + + def test_cancelled_purchase + @gateway.expects(:ssl_post).returns(failed_purchase_response_with_cancelled) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal 'ApprovedTimeOutReversal', response.message + end + def test_successful_purchase_with_elo @gateway.expects(:ssl_post).returns(successful_purchase_with_elo_response) @@ -87,7 +105,6 @@ def test_successful_purchase_with_token response = @gateway.purchase(@amount, '123456789012345678901234567890', @options) assert_success response - assert_equal 'PR-926', response.authorization assert response.test? end @@ -116,7 +133,7 @@ def test_purchase_3ds2_mpi_fields cavv: @cavv, eci: @eci, version: @three_ds_v2_version, - reference_id: @three_ds_server_trans_id, + reference_id: @directory_server_transaction_id, status: @authentication_response_status } @@ -189,13 +206,14 @@ def test_authorize_3ds1_mpi_fields end def test_authorize_3ds2_mpi_fields + @options.merge!(new_reference_id_field: true) @options[:three_d_secure] = @three_ds_v2_mpi expected_auth_data = { cavv: @cavv, eci: @eci, version: @three_ds_v2_version, - reference_id: @three_ds_server_trans_id, + reference_id: @directory_server_transaction_id, status: @authentication_response_status } @@ -254,18 +272,32 @@ def test_successful_refund response = @gateway.refund(nil, '1234', @options) assert_success response - assert response.test? + assert_equal 'Completed', response.message end def test_partial_refund response = stub_comms do @gateway.refund(@amount, '1234', @options) - end.check_request do |_endpoint, data, _headers| - assert_match(/"amount":1.0/, data) - end.respond_with(successful_refund_response) + end.respond_with(pending_response_current_status_cancelled) assert_success response - assert_equal 'Completed', response.message - assert response.test? + assert_equal 'Completed partial refunded with 1.9', response.message + end + + def test_partial_refund_with_pending_request_status + response = stub_comms do + @gateway.refund(@amount, '1234', @options) + end.respond_with(pending_response_with_pending_request_status) + assert_success response + assert_equal 'Waiting gateway confirmation for partial refund with 17480.0', response.message + end + + def test_duplicate_partial_refund + response = stub_comms do + @gateway.refund(@amount, '1234', @options) + end.respond_with(failed_pending_response_current_status) + assert_failure response + + assert_equal 'Transaction already refunded', response.message end def test_failed_refund @@ -288,7 +320,6 @@ def test_successful_void def test_failed_void @gateway.expects(:ssl_post).returns(failed_void_response) - response = @gateway.void('1234', @options) assert_equal 'Invalid Status', response.message assert_failure response @@ -341,6 +372,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 @@ -396,6 +439,7 @@ def successful_purchase_response { "transaction": { "status": "success", + "current_status": "APPROVED", "payment_date": "2017-12-19T20:29:12.715", "amount": 1, "authorization_code": "123456", @@ -423,6 +467,7 @@ def successful_purchase_with_elo_response { "transaction": { "status": "success", + "current_status": "APPROVED", "payment_date": "2019-03-06T16:47:13.430", "amount": 1, "authorization_code": "TEST00", @@ -478,6 +523,7 @@ def successful_authorize_response { "transaction": { "status": "success", + "current_status": "PENDING", "payment_date": "2017-12-21T18:04:42", "amount": 1, "authorization_code": "487897", @@ -507,6 +553,7 @@ def successful_authorize_with_elo_response { "transaction": { "status": "success", + "current_status": "PENDING", "payment_date": "2019-03-06T16:53:36.336", "amount": 1, "authorization_code": "TEST00", @@ -567,6 +614,7 @@ def successful_capture_response { "transaction": { "status": "success", + "current_status": "APPROVED", "payment_date": "2017-12-21T18:04:42", "amount": 1, "authorization_code": "487897", @@ -596,6 +644,7 @@ def successful_capture_with_elo_response { "transaction": { "status": "success", + "current_status": "APPROVED", "payment_date": "2019-03-06T16:53:36", "amount": 1, "authorization_code": "TEST00", @@ -645,7 +694,7 @@ def failed_void_response end def successful_void_response_with_more_info - '{"status": "success", "detail": "Completed", "transaction": {"carrier_code": "00", "message": "Reverse by mock"}}' + '{"status": "success", "detail": "Completed", "transaction": {"carrier_code": "00", "message": "Reverse by mock", "status_detail":7}}' end alias successful_refund_response successful_void_response @@ -727,4 +776,24 @@ def crash_response ' end + + def failed_purchase_response_with_cancelled + '{"transaction": {"id": "PR-63850089", "status": "success", "current_status": "CANCELLED", "status_detail": 29, "payment_date": "2023-12-02T22:33:48.993", "amount": 385.9, "installments": 1, "carrier_code": "00", "message": "ApprovedTimeOutReversal", "authorization_code": "097097", "dev_reference": "Order_123456789", "carrier": "Test", "product_description": "test order 1234", "payment_method_type": "7", "trace_number": "407123", "installments_type": "Revolving credit"}, "card": {"number": "4111", "bin": "11111", "type": "mc", "transaction_reference": "PR-123456", "expiry_year": "2026", "expiry_month": "12", "origin": "Paymentez", "bank_name": "CITIBANAMEX"}}' + end + + def pending_response_current_status_cancelled + '{"status": "success", "detail": "Completed partial refunded with 1.9", "transaction": {"id": "CIBC-45678", "status": "success", "current_status": "CANCELLED", "status_detail": 34, "payment_date": "2024-04-10T21:06:00", "amount": 15.544518, "installments": 1, "carrier_code": "00", "message": "Transaction Successful", "authorization_code": "000111", "dev_reference": "Order_987654_1234567899876", "carrier": "CIBC", "product_description": "referencia", "payment_method_type": "0", "trace_number": 12444, "refund_amount": 1.9}, "card": {"number": "1234", "bin": "12345", "type": "mc", "transaction_reference": "CIBC-12345", "status": "", "token": "", "expiry_year": "2028", "expiry_month": "1", "origin": "Paymentez"}}' + end + + def failed_pending_response_current_status + '{"status": "failure", "detail": "Transaction already refunded", "transaction": {"id": "CIBC-45678", "status": "success", "current_status": "APPROVED", "status_detail": 34, "payment_date": "2024-04-10T21:06:00", "amount": 15.544518, "installments": 1, "carrier_code": "00", "message": "Transaction Successful", "authorization_code": "000111", "dev_reference": "Order_987654_1234567899876", "carrier": "CIBC", "product_description": "referencia", "payment_method_type": "0", "trace_number": 12444, "refund_amount": 1.9}, "card": {"number": "1234", "bin": "12345", "type": "mc", "transaction_reference": "CIBC-12345", "status": "", "token": "", "expiry_year": "2028", "expiry_month": "1", "origin": "Paymentez"}}' + end + + def pending_response_with_pending_request_status + '{"status": "pending", "detail": "Waiting gateway confirmation for partial refund with 17480.0"}' + end + + def purchase_rejected_status + '{"transaction": {"id": "RB-14573124", "status": "failure", "current_status": "REJECTED", "status_detail": 9, "payment_date": null, "amount": 25350.0, "installments": 1, "carrier_code": "51", "message": "Fondos Insuficientes", "authorization_code": null, "dev_reference": "Order_1222223333_44445555", "carrier": "TestTest", "product_description": "Test Transaction", "payment_method_type": "7"}, "card": {"number": "4433", "bin": "54354", "type": "mc", "transaction_reference": "TT-1593752", "expiry_year": "2027", "expiry_month": "4", "origin": "Paymentez", "bank_name": "Bantest S.B."}}' + end end 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 195df830eaa..5be4717bfed 100644 --- a/test/unit/gateways/paypal_express_test.rb +++ b/test/unit/gateways/paypal_express_test.rb @@ -243,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 @@ -272,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 @@ -312,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 @@ -345,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 @@ -378,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 @@ -454,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 @@ -548,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 @@ -572,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 @@ -633,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 @@ -773,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 b961a2c2a3e..2d7c73d90ec 100644 --- a/test/unit/gateways/paysafe_test.rb +++ b/test/unit/gateways/paysafe_test.rb @@ -243,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 ed147aa7a23..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 @@ -666,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/pin_test.rb b/test/unit/gateways/pin_test.rb index bb3bb0b60c3..5cff9513e84 100644 --- a/test/unit/gateways/pin_test.rb +++ b/test/unit/gateways/pin_test.rb @@ -89,6 +89,20 @@ def test_successful_purchase assert response.test? end + def test_send_platform_adjustment + options_with_platform_adjustment = { + platform_adjustment: { + amount: 30, + currency: 'AUD' + } + } + + post = {} + @gateway.send(:add_platform_adjustment, post, @options.merge(options_with_platform_adjustment)) + assert_equal 30, post[:platform_adjustment][:amount] + assert_equal 'AUD', post[:platform_adjustment][:currency] + end + def test_unsuccessful_request @gateway.expects(:ssl_request).returns(failed_purchase_response) diff --git a/test/unit/gateways/plexo_test.rb b/test/unit/gateways/plexo_test.rb index d05db0f95cb..a673239ce48 100644 --- a/test/unit/gateways/plexo_test.rb +++ b/test/unit/gateways/plexo_test.rb @@ -9,6 +9,15 @@ def setup @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') + @network_token_credit_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + first_name: 'Santiago', last_name: 'Navatta', + brand: 'Mastercard', + payment_cryptogram: 'UnVBR0RlYm42S2UzYWJKeWJBdWQ=', + number: '5555555555554444', + source: :network_token, + month: '12', + year: 2020 + }) @options = { email: 'snavatta@plexo.com.uy', ip: '127.0.0.1', @@ -119,6 +128,24 @@ def test_successful_authorize_with_finger_print end.respond_with(successful_authorize_response) end + def test_successful_authorize_with_invoice_number + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ invoice_number: '12345abcde' })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['InvoiceNumber'], '12345abcde' + end.respond_with(successful_authorize_response) + end + + def test_successful_authorize_with_merchant_id + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge({ merchant_id: 1234 })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['MerchantId'], 1234 + end.respond_with(successful_authorize_response) + end + def test_successful_reordering_of_amount_in_authorize @gateway.expects(:ssl_post).returns(successful_authorize_response) @@ -280,6 +307,24 @@ def test_successful_verify_with_custom_amount end.respond_with(successful_verify_response) end + def test_successful_verify_with_invoice_number + stub_comms do + @gateway.verify(@credit_card, @options.merge({ invoice_number: '12345abcde' })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['InvoiceNumber'], '12345abcde' + end.respond_with(successful_verify_response) + end + + def test_successful_verify_with_merchant_id + stub_comms do + @gateway.verify(@credit_card, @options.merge({ merchant_id: 1234 })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['MerchantId'], 1234 + end.respond_with(successful_verify_response) + end + def test_failed_verify @gateway.expects(:ssl_post).returns(failed_verify_response) @@ -293,6 +338,23 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_purchase_with_network_token + purchase = stub_comms do + @gateway.purchase(@amount, @network_token_credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['Amount']['Currency'], 'UYU' + assert_equal request['Amount']['Details']['TipAmount'], '5' + assert_equal request['Flow'], 'direct' + assert_equal @network_token_credit_card.number, request['paymentMethod']['Card']['Number'] + assert_equal @network_token_credit_card.payment_cryptogram, request['paymentMethod']['Card']['Cryptogram'] + assert_equal @network_token_credit_card.first_name, request['paymentMethod']['Card']['Cardholder']['FirstName'] + end.respond_with(successful_network_token_response) + + assert_success purchase + assert_equal 'You have been mocked.', purchase.message + end + private def pre_scrubbed @@ -881,4 +943,132 @@ def failed_verify_response } RESPONSE end + + def successful_network_token_response + <<~RESPONSE + { + "id": "71d4e94a30124a7ba00809c00b7b1149", + "referenceId": "ecca673a4041317aec64e9e823b3c5d9", + "invoiceNumber": "12345abcde", + "status": "approved", + "flow": "direct", + "processingMethod": "api", + "browserDetails": { + "ipAddress": "127.0.0.1" + }, + "createdAt": "2024-05-21T20:18:33.072Z", + "updatedAt": "2024-05-21T20:18:33.3896406Z", + "processedAt": "2024-05-21T20:18:33.3896407Z", + "merchant": { + "id": 3243, + "name": "spreedly", + "settings": { + "merchantIdentificationNumber": "98001456", + "metadata": { + "paymentProcessorId": "fiserv" + }, + "paymentProcessor": { + "id": 4, + "acquirer": "fiserv" + } + }, + "clientId": 221 + }, + "client": { + "id": 221, + "name": "Spreedly", + "owner": "PLEXO" + }, + "paymentMethod": { + "id": "mastercard", + "legacyId": 4, + "name": "MASTERCARD", + "type": "card", + "card": { + "name": "555555XXXXXX4444", + "bin": "555555", + "last4": "4444", + "expMonth": 12, + "expYear": 20, + "cardholder": { + "firstName": "Santiago", + "lastName": "Navatta", + "email": "snavatta@plexo.com.uy", + "identification": { + "type": 1, + "value": "123456" + }, + "billingAddress": { + "city": "Ottawa", + "country": "CA", + "line1": "456 My Street", + "line2": "Apt 1", + "postalCode": "K1C2N6", + "state": "ON" + } + }, + "type": "prepaid", + "origin": "uruguay", + "token": "116d03bef91f4e0e8531af47ed34f690", + "issuer": { + "id": 21289, + "name": "", + "shortName": "" + }, + "tokenization": { + "type": "temporal" + } + }, + "processor": { + "id": 4, + "acquirer": "fiserv" + } + }, + "installments": 1, + "amount": { + "currency": "UYU", + "total": 1, + "details": { + "tax": { + "type": "none", + "amount": 0 + }, + "taxedAmount": 0, + "tipAmount": 5 + } + }, + "items": [ + { + "referenceId": "a6117dae92648552eb83a4ad0548833a", + "name": "prueba", + "description": "prueba desc", + "quantity": 1, + "price": 100, + "discount": 0, + "metadata": {} + } + ], + "metadata": {}, + "transactions": [ + { + "id": "664d019985707cbcfc11f0b2", + "parentId": "71d4e94a30124a7ba00809c00b7b1149", + "traceId": "c7b07c9c-d3c3-466b-8185-973321c6ab70", + "referenceId": "ecca673a4041317aec64e9e823b3c5d9", + "type": "purchase", + "status": "approved", + "createdAt": "2024-05-21T20:18:33.3896404Z", + "processedAt": "2024-05-21T20:18:33.3896397Z", + "resultCode": "0", + "resultMessage": "You have been mocked.", + "authorization": "123456", + "ticket": "02bbae8109fd4ceca0838628692486c6", + "metadata": {}, + "amount": 1 + } + ], + "actions": [] + } + RESPONSE + end end diff --git a/test/unit/gateways/priority_test.rb b/test/unit/gateways/priority_test.rb index 4d53c73417a..f3087237ff6 100644 --- a/test/unit/gateways/priority_test.rb +++ b/test/unit/gateways/priority_test.rb @@ -10,6 +10,40 @@ def setup @replay_id = rand(100...1000) @approval_message = 'Approved or completed successfully. ' @options = { billing_address: address } + @all_gateway_fields = { + is_auth: true, + invoice: '123', + source: 'test', + replay_id: @replay_id, + ship_amount: 1, + ship_to_country: 'US', + ship_to_zip: '12345', + payment_type: 'Sale', + tender_type: 'Card', + tax_exempt: true, + pos_data: { + cardholder_presence: 'NotPresent', + device_attendance: 'Unknown', + device_input_capability: 'KeyedOnly', + device_location: 'Unknown', + pan_capture_method: 'Manual', + partial_approval_support: 'Supported', + pin_capture_capability: 'Twelve' + }, + purchases: [ + { + line_item_id: 79402, + name: 'Book', + description: 'The Elements of Style', + quantity: 1, + unit_price: 1.23, + discount_amount: 0, + extended_amount: '1.23', + discount_rate: 0, + tax_amount: 1 + } + ] + } end def test_successful_purchase @@ -67,6 +101,45 @@ def test_successful_capture assert_equal '10000001625061|PaQLIYLRdWtcFKl5VaKTdUVxMutXJ5Ru', response.authorization end + def test_successful_capture_with_auth_purchase_params + stub_comms do + @gateway.capture(@amount, 'PaQLIYLRdWtcFKl5VaKTdUVxMutXJ5Ru', @all_gateway_fields) + end.check_request do |_endpoint, data, _headers| + purchase_item = @all_gateway_fields[:purchases].first + purchase_object = JSON.parse(data)['purchases'].first + response_object = JSON.parse(data) + + assert_equal(purchase_item[:name], purchase_object['name']) + assert_equal(purchase_item[:description], purchase_object['description']) + assert_equal(purchase_item[:unit_price], purchase_object['unitPrice']) + assert_equal(purchase_item[:quantity], purchase_object['quantity']) + assert_equal(purchase_item[:tax_amount], purchase_object['taxAmount']) + assert_equal(purchase_item[:discount_rate], purchase_object['discountRate']) + assert_equal(purchase_item[:discount_amount], purchase_object['discountAmount']) + assert_equal(purchase_item[:extended_amount], purchase_object['extendedAmount']) + assert_equal(purchase_item[:line_item_id], purchase_object['lineItemId']) + + assert_equal(@all_gateway_fields[:is_auth], response_object['isAuth']) + assert_equal(@all_gateway_fields[:invoice], response_object['invoice']) + assert_equal(@all_gateway_fields[:source], response_object['source']) + assert_equal(@all_gateway_fields[:replay_id], response_object['replayId']) + assert_equal(@all_gateway_fields[:ship_amount], response_object['shipAmount']) + assert_equal(@all_gateway_fields[:ship_to_country], response_object['shipToCountry']) + assert_equal(@all_gateway_fields[:ship_to_zip], response_object['shipToZip']) + assert_equal(@all_gateway_fields[:payment_type], response_object['paymentType']) + assert_equal(@all_gateway_fields[:tender_type], response_object['tenderType']) + assert_equal(@all_gateway_fields[:tax_exempt], response_object['taxExempt']) + + assert_equal(@all_gateway_fields[:pos_data][:cardholder_presence], response_object['posData']['cardholderPresence']) + assert_equal(@all_gateway_fields[:pos_data][:device_attendance], response_object['posData']['deviceAttendance']) + assert_equal(@all_gateway_fields[:pos_data][:device_input_capability], response_object['posData']['deviceInputCapability']) + assert_equal(@all_gateway_fields[:pos_data][:device_location], response_object['posData']['deviceLocation']) + assert_equal(@all_gateway_fields[:pos_data][:pan_capture_method], response_object['posData']['panCaptureMethod']) + assert_equal(@all_gateway_fields[:pos_data][:partial_approval_support], response_object['posData']['partialApprovalSupport']) + assert_equal(@all_gateway_fields[:pos_data][:pin_capture_capability], response_object['posData']['pinCaptureCapability']) + end.respond_with(successful_capture_response) + end + def test_failed_capture response = stub_comms do @gateway.capture(@amount, 'bogus_authorization', @options) diff --git a/test/unit/gateways/quickbooks_test.rb b/test/unit/gateways/quickbooks_test.rb index 7e48cce44ef..0f753f3d2ce 100644 --- a/test/unit/gateways/quickbooks_test.rb +++ b/test/unit/gateways/quickbooks_test.rb @@ -510,7 +510,7 @@ def pre_scrubbed starting SSL for sandbox.api.intuit.com:443... SSL established <- "POST /quickbooks/v4/payments/charges HTTP/1.1\r\nContent-Type: application/json\r\nRequest-Id: f8b0ce95a6e5fe249b52b23112443221\r\nAuthorization: OAuth realm=\"1292767175\", oauth_consumer_key=\"qyprdSPSxCNr5XLx0Px6g4h43zRcl6\", oauth_nonce=\"aZgGttabmZeU8ST6OjhUEMYWg7HLoyxZirBLJZVeA\", oauth_signature=\"iltPw94HHT7QCuEPTJ4RnfwY%2FzU%3D\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"1418937070\", oauth_token=\"qyprdDJJpRXRsoLDQMqaDk68c4ovXjMMVL2Wzs9RI0VNb52B\", oauth_version=\"1.0\"\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: sandbox.api.intuit.com\r\nContent-Length: 265\r\n\r\n" - <- "{\"amount\":\"1.00\",\"currency\":\"USD\",\"card\":{\"number\":\"4000100011112224\",\"expMonth\":\"09\",\"expYear\":2015,\"cvc\":\"123\",\"name\":\"Longbob Longsen\",\"address\":{\"streetAddress\":\"1234 My Street\",\"city\":\"Ottawa\",\"region\":\"CA\",\"country\":\"US\",\"postalCode\":90210}},\"capture\":\"true\"}" + <- "{\"amount\":\"1.00\",\"currency\":\"USD\",\"card\":{\"number\\\":\\\"4000100011112224\",\"expMonth\":\"09\",\"expYear\":2015,\"cvc\\\":\\\"123\",\"name\":\"Longbob Longsen\",\"address\":{\"streetAddress\":\"1234 My Street\",\"city\":\"Ottawa\",\"region\":\"CA\",\"country\":\"US\",\"postalCode\":90210}},\"capture\":\"true\"}" -> "HTTP/1.1 201 Created\r\n" -> "Date: Thu, 18 Dec 2014 21:11:11 GMT\r\n" -> "Content-Type: application/json;charset=utf-8\r\n" @@ -541,7 +541,7 @@ def post_scrubbed starting SSL for sandbox.api.intuit.com:443... SSL established <- "POST /quickbooks/v4/payments/charges HTTP/1.1\r\nContent-Type: application/json\r\nRequest-Id: f8b0ce95a6e5fe249b52b23112443221\r\nAuthorization: OAuth realm=\"[FILTERED]\", oauth_consumer_key=\"[FILTERED]\", oauth_nonce=\"[FILTERED]\", oauth_signature=\"[FILTERED]\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"1418937070\", oauth_token=\"[FILTERED]\", oauth_version=\"1.0\"\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: sandbox.api.intuit.com\r\nContent-Length: 265\r\n\r\n" - <- "{\"amount\":\"1.00\",\"currency\":\"USD\",\"card\":{\"number\":\"[FILTERED]\",\"expMonth\":\"09\",\"expYear\":2015,\"cvc\":\"[FILTERED]\",\"name\":\"Longbob Longsen\",\"address\":{\"streetAddress\":\"1234 My Street\",\"city\":\"Ottawa\",\"region\":\"CA\",\"country\":\"US\",\"postalCode\":90210}},\"capture\":\"true\"}" + <- "{\"amount\":\"1.00\",\"currency\":\"USD\",\"card\":{\"number\\\":\\\"[FILTERED]\",\"expMonth\":\"09\",\"expYear\":2015,\"cvc\\\":\\\"[FILTERED]\",\"name\":\"Longbob Longsen\",\"address\":{\"streetAddress\":\"1234 My Street\",\"city\":\"Ottawa\",\"region\":\"CA\",\"country\":\"US\",\"postalCode\":90210}},\"capture\":\"true\"}" -> "HTTP/1.1 201 Created\r\n" -> "Date: Thu, 18 Dec 2014 21:11:11 GMT\r\n" -> "Content-Type: application/json;charset=utf-8\r\n" diff --git a/test/unit/gateways/rapyd_test.rb b/test/unit/gateways/rapyd_test.rb index cbb12ce94d3..43f93789952 100644 --- a/test/unit/gateways/rapyd_test.rb +++ b/test/unit/gateways/rapyd_test.rb @@ -5,6 +5,7 @@ class RapydTest < Test::Unit::TestCase def setup @gateway = RapydGateway.new(secret_key: 'secret_key', access_key: 'access_key') + @gateway_payment_redirect = RapydGateway.new(secret_key: 'secret_key', access_key: 'access_key', url_override: 'payment_redirect') @credit_card = credit_card @check = check @amount = 100 @@ -18,27 +19,45 @@ def setup description: 'Describe this transaction', statement_descriptor: 'Statement Descriptor', email: 'test@example.com', - billing_address: address(name: 'Jim Reynolds') + billing_address: address(name: 'Jim Reynolds'), + order_id: '987654321' } @metadata = { - 'array_of_objects': [ - { 'name': 'John Doe' }, - { 'type': 'customer' } + array_of_objects: [ + { name: 'John Doe' }, + { type: 'customer' } ], - 'array_of_strings': %w[ + array_of_strings: %w[ color size ], - 'number': 1234567890, - 'object': { - 'string': 'person' + number: 1234567890, + object: { + string: 'person' }, - 'string': 'preferred', - 'Boolean': true + string: 'preferred', + Boolean: true } @ewallet_id = 'ewallet_1a867a32b47158b30a8c17d42f12f3f1' + + @address_object = address(line_1: '123 State Street', line_2: 'Apt. 34', phone_number: '12125559999') + end + + def test_request_headers_building + @options.merge!(idempotency_key: '123') + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request(skip_response: true) do |_method, _endpoint, _data, headers| + assert_equal 'application/json', headers['Content-Type'] + assert_equal '123', headers['idempotency'] + assert_equal 'access_key', headers['access_key'] + assert headers['salt'] + assert headers['signature'] + assert headers['timestamp'] + end end def test_successful_purchase @@ -52,6 +71,26 @@ def test_successful_purchase assert_equal 'payment_716ce0efc63aa8d91579e873d29d9d5e', response.authorization.split('|')[0] end + def test_send_month_and_year_with_two_digits + credit_card = credit_card('4242424242424242', month: '9', year: '30') + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, credit_card, @options) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + assert_match(/"number":"4242424242424242","expiration_month":"09","expiration_year":"30","name":"Longbob Longsen/, data) + end + 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":"09","expiration_year":"#{(Time.now.year + 1).to_s.slice(-2, 2)}","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 response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @check, @options.merge(billing_address: address(name: 'Joe John-ston'))) @@ -64,7 +103,7 @@ def test_successful_purchase_with_ach end def test_successful_purchase_with_token - @options.merge(customer_id: 'cus_9e1b5a357b2b7f25f8dd98827fbc4f22') + @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| @@ -85,6 +124,21 @@ def test_successful_purchase_with_payment_options 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 @@ -104,6 +158,74 @@ def test_successful_purchase_with_stored_credential 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['payment_method']['fields']['recurrence_type'], @options[:recurrence_type] + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_3ds_global + @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_successful_purchase_with_3ds_gateway_specific + @options.merge!(execute_threed: true, force_3d_secure: true) + + 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_nil request['payment_method_options']['3d_version'] + end.respond_with(successful_purchase_response) + end + + def test_does_not_send_3ds_version_if_not_required + false_values = [false, nil, 'false', ''] + @options[:execute_threed] = true + + false_values.each do |value| + @options[:force_3d_secure] = value + + 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_nil request['payment_method_options'] + end.respond_with(successful_purchase_response) + end + end + def test_failed_purchase @gateway.expects(:ssl_request).returns(failed_purchase_response) @@ -208,6 +330,70 @@ def test_successful_store_and_unstore assert_equal customer_id, unstore.params.dig('data', 'id') end + def test_send_receipt_email_and_customer_id_for_purchase + 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.merge(customer_id: customer_id)) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['receipt_email'], @options[:email] + assert_equal request['customer'], customer_id + assert_equal request['payment_method'], card_id + end.respond_with(successful_purchase_response) + end + + def test_send_email_with_customer_object_for_purchase + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| + request_body = JSON.parse(data) + assert request_body['customer'] + assert_equal request_body['customer']['email'], @options[:email] + end + 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 '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) @@ -220,6 +406,45 @@ def test_successful_store_with_customer_object 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: { @@ -245,8 +470,174 @@ 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 + + def test_successful_purchase_for_payment_redirect_url + @gateway_payment_redirect.expects(:ssl_request).returns(successful_purchase_response) + response = @gateway_payment_redirect.purchase(@amount, @credit_card, @options) + assert_success response + end + + def test_use_proper_url_for_payment_redirect_url + url = @gateway_payment_redirect.send(:url, 'payments', 'payment_redirect') + assert_equal url, 'https://sandboxpayment-redirect.rapyd.net/v1/payments' + end + + def test_use_proper_url_for_default_url + url = @gateway_payment_redirect.send(:url, 'payments') + assert_equal url, 'https://sandboxapi.rapyd.net/v1/payments' + end + + def test_wrong_url_for_payment_redirect_url + url = @gateway_payment_redirect.send(:url, 'refund', 'payment_redirect') + assert_no_match %r{https://sandboxpayment-redirect.rapyd.net/v1/}, url + end + + def test_add_extra_fields_for_fx_transactions + @options[:requested_currency] = 'EUR' + @options[:fixed_side] = 'buy' + + 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_equal 'EUR', request['requested_currency'] + assert_equal 'buy', request['fixed_side'] + end + end + + def test_not_add_extra_fields_for_non_fx_transactions + 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['requested_currency'] + assert_nil request['fixed_side'] + end + end + + def test_implicit_expire_unix_time + @options[:requested_currency] = 'EUR' + @options[:fixed_side] = 'buy' + + 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_in_delta 7.to_i.days.from_now.to_i, request['expiration'], 60 + end + end + + def test_sending_explicitly_expire_time + @options[:requested_currency] = 'EUR' + @options[:fixed_side] = 'buy' + @options[:expiration_days] = 2 + + 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_in_delta @options[:expiration_days].to_i.days.from_now.to_i, request['expiration'], 60 + end + end + + def test_handling_500_errors + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(response_500) + + assert_failure response + assert_equal 'some_error_message', response.message + assert_equal 'ERROR_PAYMENT_METHODS_GET', response.error_code + end + + def test_handling_500_errors_with_blank_message + response_without_message = response_500 + response_without_message.body = response_without_message.body.gsub('some_error_message', '') + + response = stub_comms(@gateway, :raw_ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(response_without_message) + + assert_failure response + assert_equal 'ERROR_PAYMENT_METHODS_GET', response.message + assert_equal 'ERROR_PAYMENT_METHODS_GET', response.error_code + end + private + def response_500 + OpenStruct.new( + code: 500, + body: { + status: { + error_code: 'ERROR_PAYMENT_METHODS_GET', + status: 'ERROR', + message: 'some_error_message', + response_code: 'ERROR_PAYMENT_METHODS_GET', + operation_id: '77703d8c-6636-48fc-bc2f-1154b5d29857' + } + }.to_json + ) + end + def pre_scrubbed ' opening connection to sandboxapi.rapyd.net:443... diff --git a/test/unit/gateways/reach_test.rb b/test/unit/gateways/reach_test.rb index 6a86450cccb..edae4bbb133 100644 --- a/test/unit/gateways/reach_test.rb +++ b/test/unit/gateways/reach_test.rb @@ -176,7 +176,7 @@ def test_stored_credential_with_no_store_credential_parameters 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)) + @gateway.expects(:get_network_payment_reference).returns(stub(message: 'abc123', success?: true)) stub_comms do @gateway.purchase(@amount, @credit_card, @options) @@ -188,7 +188,7 @@ def test_stored_credential_with_wrong_combination_stored_credential_paramaters 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)) + @gateway.expects(:get_network_payment_reference).returns(stub(message: 'abc123', success?: true)) stub_comms do @gateway.purchase(@amount, @credit_card, @options) 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_rest_test.rb b/test/unit/gateways/redsys_rest_test.rb new file mode 100644 index 00000000000..89f7cb43814 --- /dev/null +++ b/test/unit/gateways/redsys_rest_test.rb @@ -0,0 +1,312 @@ +require 'test_helper' + +class RedsysRestTest < Test::Unit::TestCase + include CommStub + + def setup + @credentials = { + login: '091952713', + secret_key: 'sq7HjrUOBfKmC576ILgskD5srU870gJ7', + terminal: '201' + } + @gateway = RedsysRestGateway.new(@credentials) + @credit_card = credit_card + @amount = 100 + + @options = { + order_id: '1001', + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_purchase + @gateway.expects(:ssl_post).returns(successful_purchase_response) + res = @gateway.purchase(123, credit_card, @options) + assert_success res + assert_equal 'Transaction Approved', res.message + assert_equal '164513224019|100|978', res.authorization + assert_equal '164513224019', res.params['ds_order'] + end + + def test_successful_purchase_requesting_credit_card_token + @gateway.expects(:ssl_post).returns(successful_purchase_response_with_credit_card_token) + res = @gateway.purchase(123, credit_card, @options) + assert_success res + assert_equal 'Transaction Approved', res.message + assert_equal '164522070945|100|978', res.authorization + assert_equal '164522070945', res.params['ds_order'] + assert_equal '2202182245100', res.params['ds_merchant_cof_txnid'] + end + + def test_successful_purchase_with_stored_credentials + @gateway.expects(:ssl_post).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 '2205022148020', 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 = { + order_id: '1002', + stored_credential: { + initial_transaction: false, + reason_type: 'unscheduled', + 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 '446527', res.params['ds_authorisationcode'] + end + + def test_successful_purchase_with_execute_threed + @gateway.expects(:ssl_post).returns(succcessful_3ds_auth_response_with_threeds_url) + @options.merge!(execute_threed: true) + res = @gateway.purchase(123, credit_card, @options) + + assert_equal res.success?, true + assert_equal res.message, 'CardConfiguration' + assert_equal res.params.include?('ds_emv3ds'), true + end + + def test_use_of_add_threeds + post = {} + @gateway.send(:add_threeds, post, @options) + assert_equal post, {} + + execute3ds_post = {} + execute3ds = @options.merge(execute_threed: true) + @gateway.send(:add_threeds, execute3ds_post, execute3ds) + assert_equal execute3ds_post.dig(:DS_MERCHANT_EMV3DS, :threeDSInfo), 'CardData' + + threeds_post = {} + execute3ds[:execute_threed] = false + execute3ds[:three_ds_2] = { + browser_info: { + accept_header: 'unknown', + depth: 100, + java: false, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown' + } + } + @gateway.send(:add_threeds, threeds_post, execute3ds) + assert_equal post.dig(:DS_MERCHANT_EMV3DS, :browserAcceptHeader), execute3ds.dig(:three_ds_2, :accept_header) + assert_equal post.dig(:DS_MERCHANT_EMV3DS, :browserScreenHeight), execute3ds.dig(:three_ds_2, :height) + end + + def test_failed_purchase + @gateway.expects(:ssl_post).returns(failed_purchase_response) + res = @gateway.purchase(123, credit_card, @options) + assert_failure res + assert_equal 'Refusal with no specific reason', res.message + assert_equal '164513457405', res.params['ds_order'] + end + + def test_purchase_without_order_id + assert_raise ArgumentError do + @gateway.purchase(123, credit_card) + end + end + + def test_error_purchase + @gateway.expects(:ssl_post).returns(error_purchase_response) + res = @gateway.purchase(123, credit_card, @options) + assert_failure res + assert_equal 'SIS0051 ERROR', res.message + end + + def test_successful_authorize + @gateway.expects(:ssl_post).returns(successful_authorize_response) + res = @gateway.authorize(123, credit_card, @options) + assert_success res + assert_equal 'Transaction Approved', res.message + assert_equal '165125433469|100|978', res.authorization + assert_equal '165125433469', res.params['ds_order'] + end + + def test_failed_authorize + @gateway.expects(:ssl_post).returns(failed_authorize_response) + res = @gateway.authorize(123, credit_card, @options) + assert_failure res + assert_equal 'Refusal with no specific reason', res.message + assert_equal '165125669647', res.params['ds_order'] + end + + def test_successful_capture + @gateway.expects(:ssl_post).returns(successful_capture_response) + res = @gateway.capture(123, '165125709531|100|978', @options) + assert_success res + assert_equal 'Refund / Confirmation approved', res.message + assert_equal '165125709531|100|978', res.authorization + assert_equal '165125709531', res.params['ds_order'] + end + + def test_error_capture + @gateway.expects(:ssl_post).returns(error_capture_response) + res = @gateway.capture(123, '165125709531|100|978', @options) + assert_failure res + assert_equal 'SIS0062 ERROR', res.message + end + + def test_successful_refund + @gateway.expects(:ssl_post).returns(successful_refund_response) + res = @gateway.refund(123, '165126074048|100|978', @options) + assert_success res + assert_equal 'Refund / Confirmation approved', res.message + assert_equal '165126074048|100|978', res.authorization + assert_equal '165126074048', res.params['ds_order'] + end + + def test_error_refund + @gateway.expects(:ssl_post).returns(error_refund_response) + res = @gateway.refund(123, '165126074048|100|978', @options) + assert_failure res + assert_equal 'SIS0057 ERROR', res.message + end + + def test_successful_void + @gateway.expects(:ssl_post).returns(successful_void_response) + res = @gateway.void('165126313156|100|978', @options) + assert_success res + assert_equal 'Cancellation Accepted', res.message + assert_equal '165126313156|100|978', res.authorization + assert_equal '165126313156', res.params['ds_order'] + end + + def test_error_void + @gateway.expects(:ssl_post).returns(error_void_response) + res = @gateway.void('165126074048|100|978', @options) + assert_failure res + assert_equal 'SIS0222 ERROR', res.message + end + + def test_successful_verify + @gateway.expects(:ssl_post).returns(successful_verify_response) + response = @gateway.verify(credit_card, @options) + assert_success response + end + + def test_unsuccessful_verify + @gateway.expects(:ssl_post).returns(failed_verify_response) + response = @gateway.verify(credit_card, @options) + assert_failure response + end + + def test_unknown_currency + assert_raise ArgumentError do + @gateway.purchase(123, credit_card, @options.merge(currency: 'HUH WUT')) + end + end + + def test_default_currency + assert_equal 'EUR', RedsysRestGateway.default_currency + end + + def test_supported_countries + assert_equal ['ES'], RedsysRestGateway.supported_countries + end + + def test_supported_cardtypes + assert_equal %i[visa master american_express jcb diners_club unionpay], RedsysRestGateway.supported_cardtypes + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def pre_scrubbed + ' + merchant_parameters: {"DS_MERCHANT_CURRENCY"=>"978", "DS_MERCHANT_AMOUNT"=>"100", "DS_MERCHANT_ORDER"=>"165126475243", "DS_MERCHANT_TRANSACTIONTYPE"=>"1", "DS_MERCHANT_PRODUCTDESCRIPTION"=>"", "DS_MERCHANT_TERMINAL"=>"3", "DS_MERCHANT_MERCHANTCODE"=>"327234688", "DS_MERCHANT_TITULAR"=>"Longbob Longsen", "DS_MERCHANT_PAN"=>"4242424242424242", "DS_MERCHANT_EXPIRYDATE"=>"2309", "DS_MERCHANT_CVV2"=>"123"} + ' + end + + def post_scrubbed + ' + merchant_parameters: {"DS_MERCHANT_CURRENCY"=>"978", "DS_MERCHANT_AMOUNT"=>"100", "DS_MERCHANT_ORDER"=>"165126475243", "DS_MERCHANT_TRANSACTIONTYPE"=>"1", "DS_MERCHANT_PRODUCTDESCRIPTION"=>"", "DS_MERCHANT_TERMINAL"=>"3", "DS_MERCHANT_MERCHANTCODE"=>"327234688", "DS_MERCHANT_TITULAR"=>"Longbob Longsen", "DS_MERCHANT_PAN"=>"[FILTERED]", "DS_MERCHANT_EXPIRYDATE"=>"2309", "DS_MERCHANT_CVV2"=>"[FILTERED]"} + ' + end + + def successful_verify_response + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIwIiwiRHNfQ3VycmVuY3kiOiI5NzgiLCJEc19PcmRlciI6IjE3MDEzNjk0NzQ1NCIsIkRzX01lcmNoYW50Q29kZSI6Ijk5OTAwODg4MSIsIkRzX1Rlcm1pbmFsIjoiMjAxIiwiRHNfUmVzcG9uc2UiOiIwMDAwIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiI1NDE4MTMiLCJEc19UcmFuc2FjdGlvblR5cGUiOiI3IiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19DYXJkTnVtYmVyIjoiNDU0ODgxKioqKioqMDAwNCIsIkRzX01lcmNoYW50RGF0YSI6IiIsIkRzX0NhcmRfQ291bnRyeSI6IjcyNCIsIkRzX0NhcmRfQnJhbmQiOiIxIiwiRHNfUHJvY2Vzc2VkUGF5TWV0aG9kIjoiMyIsIkRzX0NvbnRyb2xfMTcwMTM2OTQ3Njc2OCI6IjE3MDEzNjk0NzY3NjgifQ==\",\"Ds_Signature\":\"uoS0PJelg5_c4_7UgkYEJyatDuS3p2a-uJ3tB7SZPL4=\"}] + end + + def failed_verify_response + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIwIiwiRHNfQ3VycmVuY3kiOiI5NzgiLCJEc19PcmRlciI6IjE3MDEzNjk2NDI4NyIsIkRzX01lcmNoYW50Q29kZSI6Ijk5OTAwODg4MSIsIkRzX1Rlcm1pbmFsIjoiMjAxIiwiRHNfUmVzcG9uc2UiOiIwMTkwIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiIiLCJEc19UcmFuc2FjdGlvblR5cGUiOiI3IiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19DYXJkTnVtYmVyIjoiNDI0MjQyKioqKioqNDI0MiIsIkRzX01lcmNoYW50RGF0YSI6IiIsIkRzX0NhcmRfQ291bnRyeSI6IjgyNiIsIkRzX1Byb2Nlc3NlZFBheU1ldGhvZCI6IjMiLCJEc19Db250cm9sXzE3MDEzNjk2NDUxMjIiOiIxNzAxMzY5NjQ1MTIyIn0=\",\"Ds_Signature\":\"oaS6-Zuz6v6l-Jgs5hKDZ0tn01W9Z3gKNfhmfAGdfMo=\"}] + end + + def successful_purchase_response + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIxMDAiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTY0NTEzMjI0MDE5IiwiRHNfTWVyY2hhbnRDb2RlIjoiMzI3MjM0Njg4IiwiRHNfVGVybWluYWwiOiIzIiwiRHNfUmVzcG9uc2UiOiIwMDAwIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiI0ODgxODUiLCJEc19UcmFuc2FjdGlvblR5cGUiOiIwIiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19NZXJjaGFudERhdGEiOiIiLCJEc19DYXJkX0NvdW50cnkiOiI3MjQiLCJEc19DYXJkX0JyYW5kIjoiMSIsIkRzX1Byb2Nlc3NlZFBheU1ldGhvZCI6IjMiLCJEc19Db250cm9sXzE2NDUxMzIyNDE0NDkiOiIxNjQ1MTMyMjQxNDQ5In0=\",\"Ds_Signature\":\"63UXUOSVheJiBWxaWKih5yaVvfOSeOXAuoRUZyHBwJo=\"}] + end + + def successful_purchase_response_with_credit_card_token + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIxMDAiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTY0NTIyMDcwOTQ1IiwiRHNfTWVyY2hhbnRDb2RlIjoiMzI3MjM0Njg4IiwiRHNfVGVybWluYWwiOiIzIiwiRHNfUmVzcG9uc2UiOiIwMDAwIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiI0ODk5MTciLCJEc19UcmFuc2FjdGlvblR5cGUiOiIwIiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19NZXJjaGFudERhdGEiOiIiLCJEc19DYXJkX0NvdW50cnkiOiI3MjQiLCJEc19DYXJkX0JyYW5kIjoiMSIsIkRzX01lcmNoYW50X0NvZl9UeG5pZCI6IjIyMDIxODIyNDUxMDAiLCJEc19Qcm9jZXNzZWRQYXlNZXRob2QiOiIzIiwiRHNfQ29udHJvbF8xNjQ1MjIwNzEwNDcyIjoiMTY0NTIyMDcxMDQ3MiJ9\",\"Ds_Signature\":\"YV6W2Ym-p84q5246GK--hc-1L6Sz0tHOcMLYZtDIf-s=\"}] + end + + def successful_purchase_initial_stored_credential_response + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIxMDAiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTY1MTUyMDg4MTM3IiwiRHNfTWVyY2hhbnRDb2RlIjoiMzI3MjM0Njg4IiwiRHNfVGVybWluYWwiOiIzIiwiRHNfUmVzcG9uc2UiOiIwMDAwIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiI0NTk5MjIiLCJEc19UcmFuc2FjdGlvblR5cGUiOiIwIiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19NZXJjaGFudERhdGEiOiIiLCJEc19DYXJkX0NvdW50cnkiOiI3MjQiLCJEc19DYXJkX0JyYW5kIjoiMSIsIkRzX01lcmNoYW50X0NvZl9UeG5pZCI6IjIyMDUwMjIxNDgwMjAiLCJEc19Qcm9jZXNzZWRQYXlNZXRob2QiOiIzIiwiRHNfQ29udHJvbF8xNjUxNTIwODgyNDA5IjoiMTY1MTUyMDg4MjQwOSJ9\",\"Ds_Signature\":\"gIQ6ebPg-nXwCZ0Vld7LbSoKBXizlmaVe1djVDuVF4s=\"}] + end + + def successful_purchase_used_stored_credential_response + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIxMDAiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTY1MTUyMDg4MjQ0IiwiRHNfTWVyY2hhbnRDb2RlIjoiMzI3MjM0Njg4IiwiRHNfVGVybWluYWwiOiIzIiwiRHNfUmVzcG9uc2UiOiIwMDAwIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiI0NDY1MjciLCJEc19UcmFuc2FjdGlvblR5cGUiOiIwIiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19NZXJjaGFudERhdGEiOiIiLCJEc19DYXJkX0NvdW50cnkiOiI3MjQiLCJEc19DYXJkX0JyYW5kIjoiMSIsIkRzX1Byb2Nlc3NlZFBheU1ldGhvZCI6IjMiLCJEc19Db250cm9sXzE2NTE1MjA4ODMzMDMiOiIxNjUxNTIwODgzMzAzIn0=\",\"Ds_Signature\":\"BC3UB0Q0IgOyuXbEe8eJddK_H77XJv7d2MQr50d4v2o=\"}] + end + + def succcessful_3ds_auth_response_with_threeds_url + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19PcmRlciI6IjAzMTNTZHFrQTcxUSIsIkRzX01lcmNoYW50Q29kZSI6Ijk5OTAwODg4MSIsIkRzX1Rlcm1pbmFsIjoiMjAxIiwiRHNfVHJhbnNhY3Rpb25UeXBlIjoiMCIsIkRzX0VNVjNEUyI6eyJwcm90b2NvbFZlcnNpb24iOiIyLjEuMCIsInRocmVlRFNTZXJ2ZXJUcmFuc0lEIjoiZjEzZTRmNWUtNzcwYS00M2ZhLThhZTktY2M3ZjEwNDVkZWFiIiwidGhyZWVEU0luZm8iOiJDYXJkQ29uZmlndXJhdGlvbiIsInRocmVlRFNNZXRob2RVUkwiOiJodHRwczovL3Npcy1kLnJlZHN5cy5lcy9zaXMtc2ltdWxhZG9yLXdlYi90aHJlZURzTWV0aG9kLmpzcCJ9LCJEc19DYXJkX1BTRDIiOiJZIn0=\",\"Ds_Signature\":\"eDXoo9vInPQtJThDg1hH2ohASsUNKxd9ly8cLeK5vm0=\"}] + end + + def failed_purchase_response + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIxMDAiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTY0NTEzNDU3NDA1IiwiRHNfTWVyY2hhbnRDb2RlIjoiMzI3MjM0Njg4IiwiRHNfVGVybWluYWwiOiIzIiwiRHNfUmVzcG9uc2UiOiIwMTkwIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiIiLCJEc19UcmFuc2FjdGlvblR5cGUiOiIwIiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19NZXJjaGFudERhdGEiOiIiLCJEc19DYXJkX0NvdW50cnkiOiI4MjYiLCJEc19Qcm9jZXNzZWRQYXlNZXRob2QiOiIzIiwiRHNfQ29udHJvbF8xNjQ1MTM0NTc1MzU1IjoiMTY0NTEzNDU3NTM1NSJ9\",\"Ds_Signature\":\"zm3FCtPPhf5Do7FzlB4DbGDgkFcNFhXQCikc-batUW0=\"}] + end + + def error_purchase_response + %[{\"errorCode\":\"SIS0051\"}] + end + + def successful_authorize_response + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIxMDAiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTY1MTI1NDMzNDY5IiwiRHNfTWVyY2hhbnRDb2RlIjoiMzI3MjM0Njg4IiwiRHNfVGVybWluYWwiOiIzIiwiRHNfUmVzcG9uc2UiOiIwMDAwIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiI0NTgyNjAiLCJEc19UcmFuc2FjdGlvblR5cGUiOiIxIiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19NZXJjaGFudERhdGEiOiIiLCJEc19DYXJkX0NvdW50cnkiOiI3MjQiLCJEc19DYXJkX0JyYW5kIjoiMSIsIkRzX1Byb2Nlc3NlZFBheU1ldGhvZCI6IjMiLCJEc19Db250cm9sXzE2NTEyNTQzMzYzMTEiOiIxNjUxMjU0MzM2MzExIn0=\",\"Ds_Signature\":\"8H7F04WLREFYi67DxusWJX12NZOrMrmtDOVWYA-604M=\"}] + end + + def failed_authorize_response + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIxMDAiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTY1MTI1NjY5NjQ3IiwiRHNfTWVyY2hhbnRDb2RlIjoiMzI3MjM0Njg4IiwiRHNfVGVybWluYWwiOiIzIiwiRHNfUmVzcG9uc2UiOiIwMTkwIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiIiLCJEc19UcmFuc2FjdGlvblR5cGUiOiIxIiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19NZXJjaGFudERhdGEiOiIiLCJEc19DYXJkX0NvdW50cnkiOiI4MjYiLCJEc19Qcm9jZXNzZWRQYXlNZXRob2QiOiIzIiwiRHNfQ29udHJvbF8xNjUxMjU2Njk4MDE0IjoiMTY1MTI1NjY5ODAxNCJ9\",\"Ds_Signature\":\"abBYZFLtYloFRQDTnMhXASMcS-4SLxEBNpTfBVCBtuc=\"}] + end + + def successful_capture_response + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIxMDAiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTY1MTI1NzA5NTMxIiwiRHNfTWVyY2hhbnRDb2RlIjoiMzI3MjM0Njg4IiwiRHNfVGVybWluYWwiOiIzIiwiRHNfUmVzcG9uc2UiOiIwOTAwIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiI0NDQ5NTIiLCJEc19UcmFuc2FjdGlvblR5cGUiOiIyIiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19NZXJjaGFudERhdGEiOiIiLCJEc19DYXJkX0NvdW50cnkiOiI3MjQiLCJEc19DYXJkX0JyYW5kIjoiMSIsIkRzX1Byb2Nlc3NlZFBheU1ldGhvZCI6IjMiLCJEc19Db250cm9sXzE2NTEyNTcwOTc5NjIiOiIxNjUxMjU3MDk3OTYyIn0=\",\"Ds_Signature\":\"9lKWSe94kdviKN_ApUV9nQAS6VQc7gPeARyhpbN3sXA=\"}] + end + + def error_capture_response + %[{\"errorCode\":\"SIS0062\"}] + end + + def successful_refund_response + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIxMDAiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTY1MTI2MDc0MDQ4IiwiRHNfTWVyY2hhbnRDb2RlIjoiMzI3MjM0Njg4IiwiRHNfVGVybWluYWwiOiIzIiwiRHNfUmVzcG9uc2UiOiIwOTAwIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiI0NDQ5NjQiLCJEc19UcmFuc2FjdGlvblR5cGUiOiIzIiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19NZXJjaGFudERhdGEiOiIiLCJEc19DYXJkX0NvdW50cnkiOiI3MjQiLCJEc19DYXJkX0JyYW5kIjoiMSIsIkRzX1Byb2Nlc3NlZFBheU1ldGhvZCI6IjMiLCJEc19Db250cm9sXzE2NTEyNjA3NDM0NjAiOiIxNjUxMjYwNzQzNDYwIn0=\",\"Ds_Signature\":\"iGhvjtqbV-b3cvEoJxIwp3kE1b65onfZnF9Kb5JWWhw=\"}] + end + + def error_refund_response + %[{\"errorCode\":\"SIS0057\"}] + end + + def successful_void_response + %[{\"Ds_SignatureVersion\":\"HMAC_SHA256_V1\",\"Ds_MerchantParameters\":\"eyJEc19BbW91bnQiOiIxMDAiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTY1MTI2MzEzMTU2IiwiRHNfTWVyY2hhbnRDb2RlIjoiMzI3MjM0Njg4IiwiRHNfVGVybWluYWwiOiIzIiwiRHNfUmVzcG9uc2UiOiIwNDAwIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiI0NTgzMDQiLCJEc19UcmFuc2FjdGlvblR5cGUiOiI5IiwiRHNfU2VjdXJlUGF5bWVudCI6IjAiLCJEc19MYW5ndWFnZSI6IjEiLCJEc19NZXJjaGFudERhdGEiOiIiLCJEc19DYXJkX0NvdW50cnkiOiI3MjQiLCJEc19DYXJkX0JyYW5kIjoiMSIsIkRzX1Byb2Nlc3NlZFBheU1ldGhvZCI6IjMiLCJEc19Db250cm9sXzE2NTEyNjMxMzQzMzUiOiIxNjUxMjYzMTM0MzM1In0=\",\"Ds_Signature\":\"retARpDayWGhU-pa3OEBIT7b4iG91Mi98jHGB3EyD6c=\"}] + end + + def error_void_response + %[{\"errorCode\":\"SIS0222\"}] + end +end diff --git a/test/unit/gateways/redsys_sha256_test.rb b/test/unit/gateways/redsys_sha256_test.rb index 1bada5830d4..7fdf18331ea 100644 --- a/test/unit/gateways/redsys_sha256_test.rb +++ b/test/unit/gateways/redsys_sha256_test.rb @@ -361,20 +361,13 @@ def test_override_currency end def test_successful_verify - @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response).then.returns(successful_void_response) - response = @gateway.verify(credit_card, order_id: '144743367273') - assert_success response - end - - def test_successful_verify_with_failed_void - @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response).then.returns(failed_void_response) + @gateway.expects(:ssl_post).returns(successful_purchase_response) response = @gateway.verify(credit_card, order_id: '144743367273') assert_success response - assert_equal 'Transaction Approved', response.message end def test_unsuccessful_verify - @gateway.expects(:ssl_post).returns(failed_authorize_response) + @gateway.expects(:ssl_post).returns(failed_purchase_response) response = @gateway.verify(credit_card, order_id: '141278225678') assert_failure response assert_equal 'SIS0093 ERROR', response.message @@ -391,7 +384,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 diff --git a/test/unit/gateways/redsys_test.rb b/test/unit/gateways/redsys_test.rb index 7d87a88a96a..de36f97cae9 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("#{1.year.from_now.strftime('%y')}09")), + 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("#{1.year.from_now.strftime('%y')}09"), + 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) @@ -194,23 +316,33 @@ def test_override_currency end def test_successful_verify - @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response).then.returns(successful_void_response) - response = @gateway.verify(credit_card, @options) - assert_success response - end + @gateway.expects(:ssl_post).with( + anything, + all_of( + includes(CGI.escape('0')), + includes(CGI.escape('0')) + ), + anything + ).returns(successful_purchase_response) - def test_successful_verify_with_failed_void - @gateway.expects(:ssl_post).times(2).returns(successful_authorize_response).then.returns(failed_void_response) response = @gateway.verify(credit_card, @options) + assert_success response - assert_equal 'Transaction Approved', response.message end def test_unsuccessful_verify - @gateway.expects(:ssl_post).returns(failed_authorize_response) + @gateway.expects(:ssl_post).with( + anything, + all_of( + includes(CGI.escape('0')), + includes(CGI.escape('0')) + ), + anything + ).returns(failed_purchase_response) + response = @gateway.verify(credit_card, @options) + assert_failure response - assert_equal 'SIS0093 ERROR', response.message end def test_unknown_currency @@ -224,7 +356,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 @@ -301,7 +433,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..06ba3574287 100644 --- a/test/unit/gateways/safe_charge_test.rb +++ b/test/unit/gateways/safe_charge_test.rb @@ -108,6 +108,25 @@ def test_successful_purchase_with_falsey_stored_credential_mode assert purchase.test? end + def test_successful_purchase_with_token + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(successful_purchase_response) + assert_success response + assert_equal 'Success', response.message + + _, transaction_id = response.authorization.split('|') + subsequent_response = stub_comms do + @gateway.purchase(@amount, response.authorization, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/sg_CCToken/, data) + assert_match(/sg_TransactionID=#{transaction_id}/, data) + end.respond_with(successful_purchase_response) + + assert_success subsequent_response + assert_equal 'Success', subsequent_response.message + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) @@ -203,6 +222,46 @@ 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_successful_credit_with_unreferenced_refund + credit = stub_comms do + @gateway.credit(@amount, @credit_card, @options.merge(unreferenced_refund: true)) + end.check_request do |_endpoint, data, _headers| + assert_equal(data.split('&').include?('sg_CreditType=2'), true) + end.respond_with(successful_credit_response) + + assert_success credit + end + + def test_successful_credit_without_unreferenced_refund + credit = stub_comms do + @gateway.credit(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_equal(data.split('&').include?('sg_CreditType=1'), true) + end.respond_with(successful_credit_response) + + assert_success credit + end + def test_failed_refund @gateway.expects(:ssl_post).returns(failed_refund_response) @@ -219,6 +278,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 6abd0e9e3f9..3342ad2db46 100644 --- a/test/unit/gateways/sage_pay_test.rb +++ b/test/unit/gateways/sage_pay_test.rb @@ -50,11 +50,11 @@ def test_unsuccessful_purchase end def test_purchase_url - assert_equal 'https://test.sagepay.com/gateway/service/vspdirect-register.vsp', @gateway.send(:url_for, :purchase) + assert_equal 'https://sandbox.opayo.eu.elavon.com/gateway/service/vspdirect-register.vsp', @gateway.send(:url_for, :purchase) end def test_capture_url - assert_equal 'https://test.sagepay.com/gateway/service/release.vsp', @gateway.send(:url_for, :capture) + assert_equal 'https://sandbox.opayo.eu.elavon.com/gateway/service/release.vsp', @gateway.send(:url_for, :capture) end def test_matched_avs_result @@ -252,6 +252,15 @@ def test_protocol_version_is_honoured end.respond_with(successful_purchase_response) end + def test_override_protocol_via_transaction + options = @options.merge(protocol_version: '4.00') + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/VPSProtocol=4.00/, data) + end.respond_with(successful_purchase_response) + end + def test_referrer_id_is_added_to_post_data_parameters ActiveMerchant::Billing::SagePayGateway.application_id = '00000000-0000-0000-0000-000000000001' stub_comms(@gateway, :ssl_request) do @@ -316,9 +325,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 @@ -341,6 +348,142 @@ def test_repeat_purchase_from_reference_purchase end.respond_with(successful_purchase_response) end + def test_true_boolean_3ds_fields + options = @options.merge({ + protocol_version: '4.00', + three_ds_2: { + channel: 'browser', + browser_info: { + accept_header: 'unknown', + depth: 48, + java: true, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown', + browser_size: '05' + }, + notification_url: 'https://example.com/notification' + } + }) + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/BrowserJavascriptEnabled=1/, data) + assert_match(/BrowserJavaEnabled=1/, data) + end.respond_with(successful_purchase_response) + end + + def test_false_boolean_3ds_fields + options = @options.merge({ + protocol_version: '4.00', + three_ds_2: { + channel: 'browser', + browser_info: { + accept_header: 'unknown', + depth: 48, + java: false, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown', + browser_size: '05' + }, + notification_url: 'https://example.com/notification' + } + }) + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/BrowserJavascriptEnabled=0/, data) + assert_match(/BrowserJavaEnabled=0/, data) + end.respond_with(successful_purchase_response) + end + + def test_sending_3ds2_params + options = @options.merge({ + protocol_version: '4.00', + three_ds_2: { + channel: 'browser', + browser_info: { + accept_header: 'unknown', + depth: 48, + java: true, + language: 'US', + height: 1000, + width: 500, + timezone: '-120', + user_agent: 'unknown', + browser_size: '05' + }, + notification_url: 'https://example.com/notification' + } + }) + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/VPSProtocol=4.00/, data) + assert_match(/BrowserAcceptHeader=unknown/, data) + assert_match(/BrowserLanguage=US/, data) + assert_match(/BrowserUserAgent=unknown/, data) + assert_match(/BrowserColorDepth=48/, data) + assert_match(/BrowserScreenHeight=1000/, data) + assert_match(/BrowserScreenWidth=500/, data) + assert_match(/BrowserTZ=-120/, data) + assert_match(/ChallengeWindowSize=05/, data) + end.respond_with(successful_purchase_response) + end + + def test_sending_cit_params + options = @options.merge!({ + protocol_version: '4.00', + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'installment' + }, + recurring_frequency: '30', + recurring_expiry: "#{Time.now.year + 1}-04-21", + installment_data: 5 + }) + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/COFUsage=FIRST/, data) + assert_match(/MITType=INSTALMENT/, data) + assert_match(/RecurringFrequency=30/, data) + assert_match(/PurchaseInstalData=5/, data) + assert_match(/RecurringExpiry=#{Time.now.year + 1}-04-21/, data) + end.respond_with(successful_purchase_response) + end + + def test_sending_mit_params + options = @options.merge({ + protocol_version: '4.00', + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'recurring', + network_transaction_id: '123' + }, + recurring_frequency: '30', + recurring_expiry: "#{Time.now.year + 1}-04-21" + }) + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/COFUsage=SUBSEQUENT/, data) + assert_match(/MITType=RECURRING/, data) + assert_match(/RecurringFrequency=30/, data) + assert_match(/SchemeTraceID=123/, data) + assert_match(/RecurringExpiry=#{Time.now.year + 1}-04-21/, 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..b37509b5f66 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'] @@ -262,7 +262,7 @@ def test_failed_authorize response = @gateway.authorize(@amount, @credit_card, @options) assert_failure response assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code - assert_nil response.authorization + assert_equal 'char_mApucpvVbCJgo7x09Je4n9gC', response.authorization assert response.test? end @@ -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 index 690b92bd487..4a7c02d3605 100644 --- a/test/unit/gateways/shift4_test.rb +++ b/test/unit/gateways/shift4_test.rb @@ -1,15 +1,5 @@ require 'test_helper' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - class Shift4Gateway - def setup_access_token - '12345678' - end - end - end -end - class Shift4Test < Test::Unit::TestCase include CommStub def setup @@ -23,7 +13,8 @@ def setup tax: '2', customer_reference: 'D019D09309F2', destination_postal_code: '94719', - product_descriptors: %w(Hamburger Fries Soda Cookie) + product_descriptors: %w(Hamburger Fries Soda Cookie), + order_id: '123456' } @customer_address = { address1: '123 Street', @@ -73,6 +64,7 @@ def test_successful_purchase_with_extra_fields 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] @@ -231,6 +223,18 @@ def test_successful_refund 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'], @credit_card.expiry_date.expiration.strftime('%m%y') + 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') @@ -241,9 +245,12 @@ def test_successful_void def test_failed_purchase @gateway.expects(:ssl_request).returns(failed_purchase_response) + response = @gateway.purchase(@amount, @credit_card, @options) - response = @gateway.purchase(@amount, 'abc', @options) assert_failure response + assert_equal response.message, 'Transaction declined' + assert_equal 'A', response.avs_result['code'] + assert_equal 'Street address matches, but postal code does not match.', response.avs_result['message'] assert_nil response.authorization end @@ -256,6 +263,27 @@ def test_failed_authorize assert response.test? end + def test_failed_authorize_with_host_response + response = stub_comms do + @gateway.authorize(@amount, @credit_card) + end.respond_with(failed_authorize_with_host_response) + + assert_failure response + assert_equal 'CVV value N not accepted.', response.message + assert response.test? + end + + def test_successful_authorize_with_avs_result + response = stub_comms do + @gateway.authorize(@amount, @credit_card) + end.respond_with(successful_authorize_response) + + assert_success response + assert_equal 'Y', response.avs_result['code'] + assert_equal 'Street address and 5-digit postal code match.', response.avs_result['message'] + assert response.test? + end + def test_failed_capture @gateway.expects(:ssl_request).returns(failed_capture_response) @@ -268,9 +296,10 @@ def test_failed_capture def test_failed_refund @gateway.expects(:ssl_request).returns(failed_refund_response) - response = @gateway.refund(@amount, 'abc', @options) + response = @gateway.refund(1919, @credit_card, @options) assert_failure response - assert_nil response.authorization + assert_equal response.error_code, 'D' + assert_equal response.message, 'Transaction declined' assert response.test? end @@ -365,6 +394,22 @@ def test_scrub 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) @@ -604,6 +649,12 @@ def successful_authorize_response "transaction": { "authorizationCode": "OK168Z", "authSource": "E", + "avs": { + "postalCodeVerified":"Y", + "result":"Y", + "streetVerified":"Y", + "valid":"Y" + }, "invoice": "3333333309", "purchaseCard": { "customerReference": "457", @@ -900,18 +951,78 @@ def failed_authorize_response def failed_purchase_response <<-RESPONSE { - "result": [ + "result": [ + { + "dateTime":"2024-01-12T15:11:10.000-08:00", + "receiptColumns":30, + "amount": { + "total":15000000 + }, + "card": { + "type":"VS", + "entryMode":"M", + "number":"XXXXXXXXXXXX2224", + "present":"N", + "securityCode": { + "result":"M", + "valid":"Y" + }, + "token": { + "value":"2224028jbvt7g0ne" + } + }, + "clerk": { + "numericId":1 + }, + "customer": { + "firstName":"John", + "lastName":"Smith" + }, + "device": { + "capability": { + "magstripe":"Y", + "manualEntry":"Y" + } + }, + "merchant": { + "mid":8628968, + "name":"Spreedly - ECom" + }, + "receipt": [ { - "error": { - "longText": "Token contains invalid characters UTGAPI08CE", - "primaryCode": 9864, - "shortText": "Invalid Token" - }, - "server": { - "name": "UTGAPI08CE" - } + "key":"MaskedPAN", + "printValue":"XXXXXXXXXXXX2224" + }, + { + "key":"CardEntryMode", + "printName":"ENTRY METHOD", + "printValue":"KEYED" + }, + { + "key":"SignatureRequired", + "printValue":"N" } - ] + ], + "server": { + "name":"UTGAPI11CE" + }, + "transaction": { + "authSource":"E", + "avs": { + "postalCodeVerified":"N", + "result":"A", + "streetVerified":"Y", + "valid":"Y" + }, + "invoice":"0705626580", + "responseCode":"D", + "saleFlag":"S" + }, + "universalToken": { + "value":"400010-2F1AA405-001AA4-000026B7-1766C44E9E8" + } + } + ] } RESPONSE end @@ -938,19 +1049,68 @@ def failed_capture_response def failed_refund_response <<-RESPONSE { - "result": [ - { - "error": { - "longText": "record not posted ENGINE21CE", - "primaryCode": 9844, - "shortText": "I/O ERROR" - }, - "server": { - "name": "UTGAPI05CE" - } - } + "result": + [ + { + "dateTime": "2024-01-05T13:38:03.000-08:00", + "receiptColumns": 30, + "amount": { + "total": 19.19 + }, + "card": { + "type": "VS", + "entryMode": "M", + "number": "XXXXXXXXXXXX2224", + "present": "N", + "token": { + "value": "2224htm77ctttszk" + } + }, + "clerk": { + "numericId": 1 + }, + "device": { + "capability": { + "magstripe": "Y", + "manualEntry": "Y" + } + }, + "merchant": { + "name": "Spreedly - ECom" + }, + "receipt": [ + { + "key": "MaskedPAN", + "printValue": "XXXXXXXXXXXX2224" + }, + { + "key": "CardEntryMode", + "printName": "ENTRY METHOD", + "printValue": "KEYED" + }, + { + "key": "SignatureRequired", + "printValue": "N" + } + ], + "server": + { + "name": "UTGAPI04CE" + }, + "transaction": + { + "authSource": "E", + "invoice": "0704283292", + "responseCode": "D", + "saleFlag": "C" + }, + "universalToken": + { + "value": "400010-2F1AA405-001AA4-000026B7-1766C44E9E8" + } + } ] - } + } RESPONSE end @@ -997,4 +1157,124 @@ def successful_access_token_response } 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 + + def failed_authorize_with_host_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": { + "authSource":"E", + "avs": { + "postalCodeVerified":"Y", + "result":"Y", + "streetVerified":"Y", + "valid":"Y" + }, + "cardOnFile": { + "transactionId":"010512168564062", + "indicator":"01", + "scheduledIndicator":"02", + "usageIndicator":"01" + }, + "invoice":"0704938459384", + "hostResponse": { + "reasonCode":"N7", + "reasonDescription":"CVV value N not accepted." + }, + "responseCode":"D", + "retrievalReference":"400500170391", + "saleFlag":"S", + "vendorReference":"2490464558001" + }, + "universalToken": { + "value": "400010-2F1AA405-001AA4-000026B7-1766C44E9E8" + } + } + ] + } + 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..9a2e76b799f --- /dev/null +++ b/test/unit/gateways/shift4_v2_test.rb @@ -0,0 +1,135 @@ +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' + ) + @check = check + 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 + + def test_successful_store_and_unstore + @gateway.expects(:ssl_post).returns(successful_new_customer_response) + + store = @gateway.store(@credit_card, @options) + assert_success store + @gateway.expects(:ssl_request).returns(successful_unstore_response) + unstore = @gateway.unstore('card_YhkJQlyF6NEc9RexV5dlZqTl', customer_id: 'cust_KDDJGACwxCUYkUb3fI76ERB7') + assert_success unstore + end + + def test_successful_unstore + response = stub_comms(@gateway, :ssl_request) do + @gateway.unstore('card_YhkJQlyF6NEc9RexV5dlZqTl', customer_id: 'cust_KDDJGACwxCUYkUb3fI76ERB7') + end.check_request do |_endpoint, data, _headers| + assert_match(/cards/, data) + end.respond_with(successful_unstore_response) + assert response.success? + assert_equal response.message, 'Transaction approved' + end + + def test_purchase_with_bank_account + stub_comms do + @gateway.purchase(@amount, @check, @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + request = CGI.parse(data) + assert_equal request['paymentMethod[type]'].first, 'ach' + assert_equal request['paymentMethod[billing][name]'].first, 'Jim Smith' + assert_equal request['paymentMethod[billing][address][country]'].first, 'CA' + assert_equal request['paymentMethod[ach][account][routingNumber]'].first, '244183602' + assert_equal request['paymentMethod[ach][account][accountNumber]'].first, '15378535' + assert_equal request['paymentMethod[ach][account][accountType]'].first, 'personal_checking' + assert_equal request['paymentMethod[ach][verificationProvider]'].first, 'external' + end + 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 + + def successful_unstore_response + <<-RESPONSE + { + "id" : "card_G9xcxTDcjErIijO19SEWskN6" + } + RESPONSE + end +end diff --git a/test/unit/gateways/simetrik_test.rb b/test/unit/gateways/simetrik_test.rb index c120b2e99fe..82b91e5ac09 100644 --- a/test/unit/gateways/simetrik_test.rb +++ b/test/unit/gateways/simetrik_test.rb @@ -1,15 +1,5 @@ require 'test_helper' -module ActiveMerchant #:nodoc: - module Billing #:nodoc: - class SimetrikGateway < Gateway - def fetch_access_token - @access_token[:access_token] = SecureRandom.hex(16) - end - end - end -end - class SimetrikTest < Test::Unit::TestCase def setup @token_acquirer = 'ea890fd1-49f3-4a34-a150-192bf9a59205' @@ -17,7 +7,8 @@ def setup @gateway = SimetrikGateway.new( client_id: 'client_id', client_secret: 'client_secret_key', - audience: 'audience_url' + audience: 'audience_url', + access_token: { expires_at: Time.new.to_i } ) @credit_card = CreditCard.new( first_name: 'sergiod', @@ -75,63 +66,63 @@ def setup } @authorize_capture_expected_body = { - "forward_route": { - "trace_id": @trace_id, - "psp_extra_fields": {} + forward_route: { + trace_id: @trace_id, + psp_extra_fields: {} }, - "forward_payload": { - "user": { - "id": '123', - "email": 's@example.com' - }, - "order": { - "id": @order_id, - "description": 'a popsicle', - "installments": 1, - "datetime_local_transaction": @datetime, - "amount": { - "total_amount": 10.0, - "currency": 'USD', - "vat": 1.9 + forward_payload: { + user: { + id: '123', + email: 's@example.com' + }, + order: { + id: @order_id, + description: 'a popsicle', + installments: 1, + datetime_local_transaction: @datetime, + amount: { + total_amount: 10.0, + currency: 'USD', + vat: 1.9 } }, - "payment_method": { - "card": { - "number": '4551478422045511', - "exp_month": 12, - "exp_year": 2029, - "security_code": '111', - "type": 'visa', - "holder_first_name": 'sergiod', - "holder_last_name": 'lobob' + payment_method: { + card: { + number: '4551478422045511', + exp_month: 12, + exp_year: 2029, + security_code: '111', + type: 'visa', + holder_first_name: 'sergiod', + holder_last_name: 'lobob' } }, - "authentication": { - "three_ds_fields": { - "version": '2.1.0', - "eci": '02', - "cavv": 'jJ81HADVRtXfCBATEp01CJUAAAA', - "ds_transaction_id": '97267598-FAE6-48F2-8083-C23433990FBC', - "acs_transaction_id": '13c701a3-5a88-4c45-89e9-ef65e50a8bf9', - "xid": '00000000000000000501', - "enrolled": 'string', - "cavv_algorithm": '1', - "directory_response_status": 'Y', - "authentication_response_status": 'Y', - "three_ds_server_trans_id": '24f701e3-9a85-4d45-89e9-af67e70d8fg8' + authentication: { + three_ds_fields: { + version: '2.1.0', + eci: '02', + cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA', + ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC', + acs_transaction_id: '13c701a3-5a88-4c45-89e9-ef65e50a8bf9', + xid: '00000000000000000501', + enrolled: 'string', + cavv_algorithm: '1', + directory_response_status: 'Y', + authentication_response_status: 'Y', + three_ds_server_trans_id: '24f701e3-9a85-4d45-89e9-af67e70d8fg8' } }, - "sub_merchant": { - "merchant_id": 'string', - "extra_params": {}, - "mcc": 'string', - "name": 'string', - "address": 'string', - "postal_code": 'string', - "url": 'string', - "phone_number": 'string' - }, - "acquire_extra_options": {} + sub_merchant: { + merchant_id: 'string', + extra_params: {}, + mcc: 'string', + name: 'string', + address: 'string', + postal_code: 'string', + url: 'string', + phone_number: 'string' + }, + acquire_extra_options: {} } }.to_json.to_s end @@ -170,6 +161,15 @@ def test_success_purchase_with_billing_address assert response.test? end + def test_fetch_access_token_should_rise_an_exception_under_bad_request + error = assert_raises(ActiveMerchant::OAuthResponseError) do + @gateway.expects(:raw_ssl_request).returns(Net::HTTPBadRequest.new(1.0, 401, 'Unauthorized')) + @gateway.send(:fetch_access_token) + end + + assert_match(/Failed with 401 Unauthorized/, error.message) + 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 diff --git a/test/unit/gateways/stripe_payment_intents_test.rb b/test/unit/gateways/stripe_payment_intents_test.rb index d4d1c8d799b..cf80a066e9e 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')) @@ -191,6 +216,7 @@ def test_failed_capture_after_creation assert create = @gateway.create_intent(@amount, 'pm_card_chargeDeclined', @options.merge(confirm: true)) assert_equal 'requires_payment_method', create.params.dig('error', 'payment_intent', 'status') assert_equal false, create.params.dig('error', 'payment_intent', 'charges', 'data')[0].dig('captured') + assert_equal 'ch_1F2MB6AWOtgoysogAIvNV32Z', create.authorization end def test_failed_void_after_capture @@ -206,6 +232,14 @@ def test_failed_void_after_capture 'requires_payment_method, requires_capture, requires_confirmation, requires_action.', cancel.message end + def test_failed_verify + @gateway.expects(:add_payment_method_token).returns(@visa_token) + @gateway.expects(:ssl_request).returns(failed_verify_response) + + assert create = @gateway.verify(@credit_card) + assert_equal 'seti_nhtadoeunhtaobjntaheodu', create.authorization + end + def test_connected_account destination = 'account_27701' amount = 8000 @@ -259,6 +293,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) @@ -324,6 +380,67 @@ def test_successful_purchase_with_level3_data end.respond_with(successful_create_intent_response) end + def test_successful_purchase_with_card_brand + @options[:card_brand] = 'cartes_bancaires' + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @visa_token, @options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_options[card][network]=cartes_bancaires', 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' @@ -355,7 +472,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| @@ -391,18 +508,69 @@ 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' + currency: 'GBP', + new_ap_gp_route: true } + @google_pay.eci = '5' + assert_match('5', @google_pay.eci) + stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @google_pay, options) - end.check_request do |_method, endpoint, data, _headers| - assert_match('card[tokenization_method]=android_pay', data) if %r{/tokens}.match?(endpoint) - assert_match('payment_method=pi_', data) if %r{/payment_intents}.match?(endpoint) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_options[card][network_token][electronic_commerce_indicator]=05', data) + assert_match('payment_method_data[card][network_token][tokenization_method]=google_pay_dpan', data) end.respond_with(successful_create_intent_response) end + def test_purchase_with_google_pay_with_billing_address + options = { + currency: 'GBP', + billing_address: address, + new_ap_gp_route: true + } + @google_pay.eci = nil + + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @google_pay, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_not_match('payment_method_options[card][network_token][electronic_commerce_indicator]', data) + assert_match('payment_method_data[billing_details][name]=Jim+Smith', data) + assert_match('payment_method_data[card][network_token][tokenization_method]=google_pay_dpan', data) + 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', @@ -415,7 +583,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 @@ -429,13 +598,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' @@ -443,6 +612,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| @@ -455,16 +625,32 @@ def test_purchase_with_shipping_carrier_and_tracking_number def test_authorize_with_apple_pay options = { - currency: 'GBP' + currency: 'GBP', + new_ap_gp_route: true } 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('payment_method=pi_', data) if %r{/payment_intents}.match?(endpoint) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_data[card][network_token][tokenization_method]=apple_pay', data) + assert_match('payment_method_options[card][network_token][electronic_commerce_indicator]=05', data) end.respond_with(successful_create_intent_response) end + def test_authorize_with_apple_pay_with_billing_address + options = { + currency: 'GBP', + billing_address: address, + new_ap_gp_route: true + } + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @apple_pay, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_data[card][network_token][tokenization_method]=apple_pay', data) + assert_match('payment_method_options[card][network_token][electronic_commerce_indicator]=05', data) + assert_match('payment_method_data[billing_details][address][line1]=456+My+Street', data) + 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' @@ -623,6 +809,123 @@ def test_scrub_filter_token assert_equal @gateway.scrub(pre_scrubbed), scrubbed end + def test_scrub_apple_pay + assert_equal @gateway.scrub(pre_scrubbed_apple_pay), scrubbed_apple_pay + 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 + + def test_successful_avs_and_cvc_check + @gateway.expects(:ssl_request).returns(successful_purchase_avs_pass) + options = {} + assert purchase = @gateway.purchase(@amount, @visa_card, options) + + assert_equal 'succeeded', purchase.params['status'] + assert_equal 'M', purchase.cvv_result.dig('code') + assert_equal 'CVV matches', purchase.cvv_result.dig('message') + assert_equal 'Y', purchase.avs_result.dig('code') + end + private def successful_setup_purchase @@ -695,6 +998,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} @@ -991,6 +1834,12 @@ def failed_cancel_response RESPONSE end + def failed_verify_response + <<-RESPONSE + {"error":{"code":"incorrect_cvc","doc_url":"https://stripe.com/docs/error-codes/incorrect-cvc","message":"Yourcard'ssecuritycodeisincorrect.","param":"cvc","payment_method":{"id":"pm_11111111111111111111","object":"payment_method","billing_details":{"address":{"city":null,"country":null,"line1":null,"line2":null,"postal_code":"12345","state":null},"email":null,"name":"Andrew Earl","phone":null},"card":{"brand":"visa","checks":{"address_line1_check":null,"address_postal_code_check":"pass","cvc_check":"fail"},"country":"US","description":null,"display_brand":{"label":"Visa","logo_url":"https://b.stripecdn.com/cards-metadata/logos/card-visa.svg","type":"visa"},"exp_month":11,"exp_year":2027,"fingerprint":"SuqLiaoeuthnaomyEJhqjSl","funding":"credit","generated_from":null,"iin":"4111111","issuer":"StripeTest(multi-country)","last4":"1111","networks":{"available":["visa"],"preferred":null},"three_d_secure_usage":{"supported":true},"wallet":null},"created":1706803138,"customer":null,"livemode":false,"metadata":{},"type":"card"},"request_log_url":"https://dashboard.stripe.com/acct_1EzHCMD9l2v51lHE/test/logs/req_7bcL8JaztST1Ho?t=1706803138","setup_intent":{"id":"seti_nhtadoeunhtaobjntaheodu","object":"setup_intent","application":"ca_aotnheudnaoethud","automatic_payment_methods":null,"cancellation_reason":null,"client_secret":"seti_nhtadoeunhtaobjntaheodu_secret_aoentuhaosneutkmanotuheidna","created":1706803138,"customer":null,"description":null,"flow_directions":null,"last_setup_error":{"code":"incorrect_cvc","doc_url":"https://stripe.com/docs/error-codes/incorrect-cvc","message":"Your cards security code is incorrect.","param":"cvc","payment_method":{"id":"pm_11111111111111111111","object":"payment_method","billing_details":{"address":{"city":null,"country":null,"line1":null,"line2":null,"postal_code":"12345","state":null},"email":null,"name":"AndrewEarl","phone":null},"card":{"brand":"visa","checks":{"address_line1_check":null,"address_postal_code_check":"pass","cvc_check":"fail"},"country":"US","description":null,"display_brand":{"label":"Visa","logo_url":"https://b.stripecdn.com/cards-metadata/logos/card-visa.svg","type":"visa"},"exp_month":11,"exp_year":2027,"fingerprint":"anotehjbnaroetug","funding":"credit","generated_from":null,"iin":"411111111","issuer":"StripeTest(multi-country)","last4":"1111","networks":{"available":["visa"],"preferred":null},"three_d_secure_usage":{"supported":true},"wallet":null},"created":1706803138,"customer":null,"livemode":false,"metadata":{},"type":"card"},"type":"card_error"},"latest_attempt":"setatt_ansotheuracogeudna","livemode":false,"mandate":null,"metadata":{"transaction_token":"ntahodejrcagoedubntha","order_id":"ntahodejrcagoedubntha","connect_agent":"Spreedly"},"next_action":null,"on_behalf_of":null,"payment_method":null,"payment_method_configuration_details":null,"payment_method_options":{"card":{"mandate_options":null,"network":null,"request_three_d_secure":"automatic"}},"payment_method_types":["card"],"single_use_mandate":null,"status":"requires_payment_method","usage":"off_session"},"type":"card_error"}} + RESPONSE + end + def failed_payment_method_response <<-RESPONSE {"error": {"code": "validation_error", "message": "You must verify a phone number on your Stripe account before you can send raw credit card numbers to the Stripe API. You can avoid this requirement by using Stripe.js, the Stripe mobile bindings, or Stripe Checkout. For more information, see https://dashboard.stripe.com/phone-verification.", "type": "invalid_request_error"}} @@ -1273,4 +2122,165 @@ def scrubbed Conn close SCRUBBED end + + def pre_scrubbed_apple_pay + <<-PRE_SCRUBBED + opening connection to api.stripe.com:443... + opened + starting SSL for api.stripe.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- \"POST /v1/payment_intents HTTP/1.1\\r\\nContent-Type: application/x-www-form-urlencoded\\r\\nAuthorization: Basic c2tfdGVzdF81MTYwRFg2QVdPdGdveXNvZ0JvcHRXN2xpeEtFeHozNlJ1bnRlaHU4WUw4RWRZT2dqaXlkaFpVTEMzaEJzdmQ0Rk90d1RtNTd3WjRRNVZtTkY5enJJV0tvRzAwOFQxNzZHOG46\\r\\nUser-Agent: Stripe/v1 ActiveMerchantBindings/1.135.0\\r\\nStripe-Version: 2020-08-27\\r\\nX-Stripe-Client-User-Agent: {\\\"bindings_version\\\":\\\"1.135.0\\\",\\\"lang\\\":\\\"ruby\\\",\\\"lang_version\\\":\\\"3.1.3 p185 (2022-11-24)\\\",\\\"platform\\\":\\\"arm64-darwin22\\\",\\\"publisher\\\":\\\"active_merchant\\\",\\\"application\\\":{\\\"name\\\":\\\"Spreedly/ActiveMerchant\\\",\\\"version\\\":\\\"1.0/1.135.0\\\",\\\"url\\\":\\\"https://spreedly.com\\\"}}\\r\\nX-Stripe-Client-User-Metadata: {\\\"ip\\\":\\\"127.0.0.1\\\"}\\r\\nX-Transaction-Powered-By: Spreedly\\r\\nConnection: close\\r\\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\\nAccept: */*\\r\\nHost: api.stripe.com\\r\\nContent-Length: 838\\r\\n\\r\\n\" + <- \"amount=50¤cy=usd&capture_method=automatic&payment_method_data[type]=card&payment_method_data[card][last4]=4242&payment_method_data[card][exp_month]=9&payment_method_data[card][exp_year]=2024&payment_method_data[card][network_token][number]=4242424242424242&payment_method_data[card][network_token][exp_month]=9&payment_method_data[card][network_token][exp_year]=2024&payment_method_data[card][network_token][tokenization_method]=apple_pay&payment_method_options[card][network_token][cryptogram]=AMwBRjPWDnAgAA7Rls7mAoABFA%3D%3D&metadata[connect_agent]=placeholder&metadata[transaction_token]=WmaAqGg0LW0ahLEvwIkMMCAKHKe&metadata[order_id]=9900a089-9ce6-4158-9605-10b5633d1d57&confirm=true&return_url=http%3A%2F%2Fcore.spreedly.invalid%2Ftransaction%2FWmaAqGg0LW0ahLEvwIkMMCAKHKe%2Fredirect&expand[0]=charges.data.balance_transaction\" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx\r\n" + -> "Date: Fri, 14 Jan 2022 15:34:39 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 5204\r\n" + -> "Connection: close\r\n" + -> "access-control-allow-credentials: true\r\n" + -> "access-control-allow-methods: GET, POST, HEAD, OPTIONS, DELETE\r\n" + -> "access-control-allow-origin: *\r\n" + -> "access-control-expose-headers: Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required\r\n" + -> "access-control-max-age: 300\r\n" + -> "cache-control: no-cache, no-store\r\n" + -> "idempotency-key: 87bd1ae5-1cf2-4735-85e0-c8cdafb25fff\r\n" + -> "original-request: req_VkIqZgctQBI9yo\r\n" + -> "request-id: req_VkIqZgctQBI9yo\r\n" + -> "stripe-should-retry: false\r\n" + -> "stripe-version: 2020-08-27\r\n" + -> \"{\\n \\\"id\\\": \\\"pi_3P1UIQAWOtgoysog22LYv5Ie\\\",\\n \\\"object\\\": \\\"payment_intent\\\",\\n \\\"amount\\\": 50,\\n \\\"amount_capturable\\\": 0,\\n \\\"amount_details\\\": {\\n \\\"tip\\\": {}\\n },\\n \\\"amount_received\\\": 50,\\n \\\"application\\\": null,\\n \\\"application_fee_amount\\\": null,\\n \\\"automatic_payment_methods\\\": null,\\n \\\"canceled_at\\\": null,\\n \\\"cancellation_reason\\\": null,\\n \\\"capture_method\\\": \\\"automatic\\\",\\n \\\"charges\\\": {\\n \\\"object\\\": \\\"list\\\",\\n \\\"data\\\": [\\n {\\n \\\"id\\\": \\\"ch_3P1UIQAWOtgoysog2zDy9BAh\\\",\\n \\\"object\\\": \\\"charge\\\",\\n \\\"amount\\\": 50,\\n \\\"amount_captured\\\": 50,\\n \\\"amount_refunded\\\": 0,\\n \\\"application\\\": null,\\n \\\"application_fee\\\": null,\\n \\\"application_fee_amount\\\": null,\\n \\\"balance_transaction\\\": {\\n \\\"id\\\": \\\"txn_3P1UIQAWOtgoysog26U2VWBy\\\",\\n \\\"object\\\": \\\"balance_transaction\\\",\\n \\\"amount\\\": 50,\\n \\\"available_on\\\": 1712707200,\\n \\\"created\\\": 1712152571,\\n \\\"currency\\\": \\\"usd\\\",\\n \\\"description\\\": null,\\n \\\"exchange_rate\\\": null,\\n \\\"fee\\\": 31,\\n \\\"fee_details\\\": [\\n {\\n \\\"amount\\\": 31,\\n \\\"application\\\": null,\\n \\\"currency\\\": \\\"usd\\\",\\n \\\"description\\\": \\\"Stripe processing fees\\\",\\n \\\"type\\\": \\\"stripe_fee\\\"\\n }\\n ],\\n \\\"net\\\": 19,\\n \\\"reporting_category\\\": \\\"charge\\\",\\n \\\"source\\\": \\\"ch_3P1UIQAWOtgoysog2zDy9BAh\\\",\\n \\\"status\\\": \\\"pending\\\",\\n \\\"type\\\": \\\"charge\\\"\\n },\\n \\\"billing_details\\\": {\\n \\\"address\\\": {\\n \\\"city\\\": null,\\n \\\"country\\\": null,\\n \\\"line1\\\": null,\\n \\\"line2\\\": null,\\n \\\"postal_code\\\": null,\\n \\\"state\\\": null\\n },\\n \\\"email\\\": null,\\n \\\"name\\\": null,\\n \\\"phone\\\": null\\n },\\n \\\"calculated_statement_descriptor\\\": \\\"TEST\\\",\\n \\\"captured\\\": true,\\n \\\"created\\\": 1712152571,\\n \\\"currency\\\": \\\"usd\\\",\\n \\\"customer\\\": null,\\n \\\"description\\\": null,\\n \\\"destination\\\": null,\\n \\\"dispute\\\": null,\\n \\\"disputed\\\": false,\\n \\\"failure_balance_transaction\\\": null,\\n \\\"failure_code\\\": null,\\n \\\"failure_message\\\": null,\\n \\\"fraud_details\\\": {},\\n \\\"invoice\\\": null,\\n \\\"livemode\\\": false,\\n \\\"metadata\\\": {\\n \\\"connect_agent\\\": \\\"placeholder\\\",\\n \\\"order_id\\\": \\\"9900a089-9ce6-4158-9605-10b5633d1d57\\\",\\n \\\"transaction_token\\\": \\\"WmaAqGg0LW0ahLEvwIkMMCAKHKe\\\"\\n },\\n \\\"on_behalf_of\\\": null,\\n \\\"order\\\": null,\\n \\\"outcome\\\": {\\n \\\"network_status\\\": \\\"approved_by_network\\\",\\n \\\"reason\\\": null,\\n \\\"risk_level\\\": \\\"normal\\\",\\n \\\"risk_score\\\": 2,\\n \\\"seller_message\\\": \\\"Payment complete.\\\",\\n \\\"type\\\": \\\"authorized\\\"\\n },\\n \\\"paid\\\": true,\\n \\\"payment_intent\\\": \\\"pi_3P1UIQAWOtgoysog22LYv5Ie\\\",\\n \\\"payment_method\\\": \\\"pm_1P1UIQAWOtgoysogLERqyfg0\\\",\\n \\\"payment_method_details\\\": {\\n \\\"card\\\": {\\n \\\"amount_authorized\\\": 50,\\n \\\"brand\\\": \\\"visa\\\",\\n \\\"checks\\\": {\\n \\\"address_line1_check\\\": null,\\n \\\"address_postal_code_check\\\": null,\\n \\\"cvc_check\\\": null\\n },\\n \\\"country\\\": \\\"US\\\",\\n \\\"ds_transaction_id\\\": null,\\n \\\"exp_month\\\": 9,\\n \\\"exp_year\\\": 2024,\\n \\\"extended_authorization\\\": {\\n \\\"status\\\": \\\"disabled\\\"\\n },\\n \\\"fingerprint\\\": null,\\n \\\"funding\\\": \\\"credit\\\",\\n \\\"incremental_authorization\\\": {\\n \\\"status\\\": \\\"unavailable\\\"\\n },\\n \\\"installments\\\": null,\\n \\\"last4\\\": \\\"4242\\\",\\n \\\"mandate\\\": null,\\n \\\"moto\\\": null,\\n \\\"multicapture\\\": {\\n \\\"status\\\": \\\"unavailable\\\"\\n },\\n \\\"network\\\": \\\"visa\\\",\\n \\\"network_token\\\": {\\n \\\"exp_month\\\": 9,\\n \\\"exp_year\\\": 2024,\\n \\\"fingerprint\\\": \\\"hfaVNMiXc0dYSiC5\\\",\\n \\\"last4\\\": \\\"4242\\\",\\n \\\"tokenization_method\\\": \\\"apple_pay\\\",\\n \\\"used\\\": false\\n },\\n \\\"network_transaction_id\\\": \\\"104102978678771\\\",\\n \\\"overcapture\\\": {\\n \\\"maximum_amount_capturable\\\": 50,\\n \\\"status\\\": \\\"unavailable\\\"\\n },\\n \\\"three_d_secure\\\": null,\\n \\\"wallet\\\": {\\n \\\"apple_pay\\\": {\\n \\\"type\\\": \\\"apple_pay\\\"\\n },\\n \\\"dynamic_last4\\\": \\\"4242\\\",\\n \\\"type\\\": \\\"apple_pay\\\"\\n }\\n },\\n \\\"type\\\": \\\"card\\\"\\n },\\n \\\"radar_options\\\": {},\\n \\\"receipt_email\\\": null,\\n \\\"receipt_number\\\": null,\\n \\\"receipt_url\\\": \\\"https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xNjBEWDZBV090Z295c29nKPu_tbAGMgb1i-5uogg6LBYtHz5nv48TLnQFKbUhbQOjDLetYGrcnmnG64XzKTY69nso826Kd0cANL-w\\\",\\n \\\"refunded\\\": false,\\n \\\"refunds\\\": {\\n \\\"object\\\": \\\"list\\\",\\n \\\"data\\\": [],\\n \\\"has_more\\\": false,\\n \\\"total_count\\\": 0,\\n \\\"url\\\": \\\"/v1/charges/ch_3P1UIQAWOtgoysog2zDy9BAh/refunds\\\"\\n },\\n \\\"review\\\": null,\\n \\\"shipping\\\": null,\\n \\\"source\\\": null,\\n \\\"source_transfer\\\": null,\\n \\\"statement_descriptor\\\": null,\\n \\\"statement_descriptor_suffix\\\": null,\\n \\\"status\\\": \\\"succeeded\\\",\\n \\\"transfer_data\\\": null,\\n \\\"transfer_group\\\": null\\n }\\n ],\\n \\\"has_more\\\": false,\\n \\\"total_count\\\": 1,\\n \\\"url\\\": \\\"/v1/charges?payment_intent=pi_3P1UIQAWOtgoysog22LYv5Ie\\\"\\n },\\n \\\"client_secret\\\": \\\"pi_3P1UIQAWOtgoysog22LYv5Ie_secret_BXrSnt0ALWlIKXABbi8BoFJm0\\\",\\n \\\"confirmation_method\\\": \\\"automatic\\\",\\n \\\"created\\\": 1712152570,\\n \\\"currency\\\": \\\"usd\\\",\\n \\\"customer\\\": null,\\n \\\"description\\\": null,\\n \\\"invoice\\\": null,\\n \\\"last_payment_error\\\": null,\\n \\\"latest_charge\\\": \\\"ch_3P1UIQAWOtgoysog2zDy9BAh\\\",\\n \\\"level3\\\": null,\\n \\\"livemode\\\": false,\\n \\\"metadata\\\": {\\n \\\"connect_agent\\\": \\\"placeholder\\\",\\n \\\"order_id\\\": \\\"9900a089-9ce6-4158-9605-10b5633d1d57\\\",\\n \\\"transaction_token\\\": \\\"WmaAqGg0LW0ahLEvwIkMMCAKHKe\\\"\\n },\\n \\\"next_action\\\": null,\\n \\\"on_behalf_of\\\": null,\\n \\\"payment_method\\\": \\\"pm_1P1UIQAWOtgoysogLERqyfg0\\\",\\n \\\"payment_method_configuration_details\\\": null,\\n \\\"payment_method_options\\\": {\\n \\\"card\\\": {\\n \\\"installments\\\": null,\\n \\\"mandate_options\\\": null,\\n \\\"network\\\": null,\\n \\\"request_three_d_secure\\\": \\\"automatic\\\"\\n }\\n },\\n \\\"payment_method_types\\\": [\\n \\\"card\\\"\\n ],\\n \\\"processing\\\": null,\\n \\\"receipt_email\\\": null,\\n \\\"review\\\": null,\\n \\\"setup_future_usage\\\": null,\\n \\\"shipping\\\": null,\\n \\\"source\\\": null,\\n \\\"statement_descriptor\\\": null,\\n \\\"statement_descriptor_suffix\\\": null,\\n \\\"status\\\": \\\"succeeded\\\",\\n \\\"transfer_data\\\": null,\\n \\\"transfer_group\\\": null\\n}\" + read 6581 bytes + Conn close\n" + PRE_SCRUBBED + end + + def scrubbed_apple_pay + <<-SCRUBBED + opening connection to api.stripe.com:443... + opened + starting SSL for api.stripe.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + <- \"POST /v1/payment_intents HTTP/1.1\\r\\nContent-Type: application/x-www-form-urlencoded\\r\\nAuthorization: Basic [FILTERED]\\r\\nUser-Agent: Stripe/v1 ActiveMerchantBindings/1.135.0\\r\\nStripe-Version: 2020-08-27\\r\\nX-Stripe-Client-User-Agent: {\\\"bindings_version\\\":\\\"1.135.0\\\",\\\"lang\\\":\\\"ruby\\\",\\\"lang_version\\\":\\\"3.1.3 p185 (2022-11-24)\\\",\\\"platform\\\":\\\"arm64-darwin22\\\",\\\"publisher\\\":\\\"active_merchant\\\",\\\"application\\\":{\\\"name\\\":\\\"Spreedly/ActiveMerchant\\\",\\\"version\\\":\\\"1.0/1.135.0\\\",\\\"url\\\":\\\"https://spreedly.com\\\"}}\\r\\nX-Stripe-Client-User-Metadata: {\\\"ip\\\":\\\"127.0.0.1\\\"}\\r\\nX-Transaction-Powered-By: Spreedly\\r\\nConnection: close\\r\\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\\nAccept: */*\\r\\nHost: api.stripe.com\\r\\nContent-Length: 838\\r\\n\\r\\n\" + <- \"amount=50¤cy=usd&capture_method=automatic&payment_method_data[type]=card&payment_method_data[card][last4]=4242&payment_method_data[card][exp_month]=9&payment_method_data[card][exp_year]=2024&payment_method_data[card][network_token][number]=[FILTERED]&payment_method_data[card][network_token][exp_month]=9&payment_method_data[card][network_token][exp_year]=2024&payment_method_data[card][network_token][tokenization_method]=apple_pay&payment_method_options[card][network_token][cryptogram]=[FILTERED]metadata[connect_agent]=placeholder&metadata[transaction_token]=WmaAqGg0LW0ahLEvwIkMMCAKHKe&metadata[order_id]=9900a089-9ce6-4158-9605-10b5633d1d57&confirm=true&return_url=http%3A%2F%2Fcore.spreedly.invalid%2Ftransaction%2FWmaAqGg0LW0ahLEvwIkMMCAKHKe%2Fredirect&expand[0]=charges.data.balance_transaction\" + -> "HTTP/1.1 200 OK\r\n" + -> "Server: nginx\r\n" + -> "Date: Fri, 14 Jan 2022 15:34:39 GMT\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 5204\r\n" + -> "Connection: close\r\n" + -> "access-control-allow-credentials: true\r\n" + -> "access-control-allow-methods: GET, POST, HEAD, OPTIONS, DELETE\r\n" + -> "access-control-allow-origin: *\r\n" + -> "access-control-expose-headers: Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required\r\n" + -> "access-control-max-age: 300\r\n" + -> "cache-control: no-cache, no-store\r\n" + -> "idempotency-key: 87bd1ae5-1cf2-4735-85e0-c8cdafb25fff\r\n" + -> "original-request: req_VkIqZgctQBI9yo\r\n" + -> "request-id: req_VkIqZgctQBI9yo\r\n" + -> "stripe-should-retry: false\r\n" + -> "stripe-version: 2020-08-27\r\n" + -> \"{\\n \\\"id\\\": \\\"pi_3P1UIQAWOtgoysog22LYv5Ie\\\",\\n \\\"object\\\": \\\"payment_intent\\\",\\n \\\"amount\\\": 50,\\n \\\"amount_capturable\\\": 0,\\n \\\"amount_details\\\": {\\n \\\"tip\\\": {}\\n },\\n \\\"amount_received\\\": 50,\\n \\\"application\\\": null,\\n \\\"application_fee_amount\\\": null,\\n \\\"automatic_payment_methods\\\": null,\\n \\\"canceled_at\\\": null,\\n \\\"cancellation_reason\\\": null,\\n \\\"capture_method\\\": \\\"automatic\\\",\\n \\\"charges\\\": {\\n \\\"object\\\": \\\"list\\\",\\n \\\"data\\\": [\\n {\\n \\\"id\\\": \\\"ch_3P1UIQAWOtgoysog2zDy9BAh\\\",\\n \\\"object\\\": \\\"charge\\\",\\n \\\"amount\\\": 50,\\n \\\"amount_captured\\\": 50,\\n \\\"amount_refunded\\\": 0,\\n \\\"application\\\": null,\\n \\\"application_fee\\\": null,\\n \\\"application_fee_amount\\\": null,\\n \\\"balance_transaction\\\": {\\n \\\"id\\\": \\\"txn_3P1UIQAWOtgoysog26U2VWBy\\\",\\n \\\"object\\\": \\\"balance_transaction\\\",\\n \\\"amount\\\": 50,\\n \\\"available_on\\\": 1712707200,\\n \\\"created\\\": 1712152571,\\n \\\"currency\\\": \\\"usd\\\",\\n \\\"description\\\": null,\\n \\\"exchange_rate\\\": null,\\n \\\"fee\\\": 31,\\n \\\"fee_details\\\": [\\n {\\n \\\"amount\\\": 31,\\n \\\"application\\\": null,\\n \\\"currency\\\": \\\"usd\\\",\\n \\\"description\\\": \\\"Stripe processing fees\\\",\\n \\\"type\\\": \\\"stripe_fee\\\"\\n }\\n ],\\n \\\"net\\\": 19,\\n \\\"reporting_category\\\": \\\"charge\\\",\\n \\\"source\\\": \\\"ch_3P1UIQAWOtgoysog2zDy9BAh\\\",\\n \\\"status\\\": \\\"pending\\\",\\n \\\"type\\\": \\\"charge\\\"\\n },\\n \\\"billing_details\\\": {\\n \\\"address\\\": {\\n \\\"city\\\": null,\\n \\\"country\\\": null,\\n \\\"line1\\\": null,\\n \\\"line2\\\": null,\\n \\\"postal_code\\\": null,\\n \\\"state\\\": null\\n },\\n \\\"email\\\": null,\\n \\\"name\\\": null,\\n \\\"phone\\\": null\\n },\\n \\\"calculated_statement_descriptor\\\": \\\"TEST\\\",\\n \\\"captured\\\": true,\\n \\\"created\\\": 1712152571,\\n \\\"currency\\\": \\\"usd\\\",\\n \\\"customer\\\": null,\\n \\\"description\\\": null,\\n \\\"destination\\\": null,\\n \\\"dispute\\\": null,\\n \\\"disputed\\\": false,\\n \\\"failure_balance_transaction\\\": null,\\n \\\"failure_code\\\": null,\\n \\\"failure_message\\\": null,\\n \\\"fraud_details\\\": {},\\n \\\"invoice\\\": null,\\n \\\"livemode\\\": false,\\n \\\"metadata\\\": {\\n \\\"connect_agent\\\": \\\"placeholder\\\",\\n \\\"order_id\\\": \\\"9900a089-9ce6-4158-9605-10b5633d1d57\\\",\\n \\\"transaction_token\\\": \\\"WmaAqGg0LW0ahLEvwIkMMCAKHKe\\\"\\n },\\n \\\"on_behalf_of\\\": null,\\n \\\"order\\\": null,\\n \\\"outcome\\\": {\\n \\\"network_status\\\": \\\"approved_by_network\\\",\\n \\\"reason\\\": null,\\n \\\"risk_level\\\": \\\"normal\\\",\\n \\\"risk_score\\\": 2,\\n \\\"seller_message\\\": \\\"Payment complete.\\\",\\n \\\"type\\\": \\\"authorized\\\"\\n },\\n \\\"paid\\\": true,\\n \\\"payment_intent\\\": \\\"pi_3P1UIQAWOtgoysog22LYv5Ie\\\",\\n \\\"payment_method\\\": \\\"pm_1P1UIQAWOtgoysogLERqyfg0\\\",\\n \\\"payment_method_details\\\": {\\n \\\"card\\\": {\\n \\\"amount_authorized\\\": 50,\\n \\\"brand\\\": \\\"visa\\\",\\n \\\"checks\\\": {\\n \\\"address_line1_check\\\": null,\\n \\\"address_postal_code_check\\\": null,\\n \\\"cvc_check\\\": null\\n },\\n \\\"country\\\": \\\"US\\\",\\n \\\"ds_transaction_id\\\": null,\\n \\\"exp_month\\\": 9,\\n \\\"exp_year\\\": 2024,\\n \\\"extended_authorization\\\": {\\n \\\"status\\\": \\\"disabled\\\"\\n },\\n \\\"fingerprint\\\": null,\\n \\\"funding\\\": \\\"credit\\\",\\n \\\"incremental_authorization\\\": {\\n \\\"status\\\": \\\"unavailable\\\"\\n },\\n \\\"installments\\\": null,\\n \\\"last4\\\": \\\"4242\\\",\\n \\\"mandate\\\": null,\\n \\\"moto\\\": null,\\n \\\"multicapture\\\": {\\n \\\"status\\\": \\\"unavailable\\\"\\n },\\n \\\"network\\\": \\\"visa\\\",\\n \\\"network_token\\\": {\\n \\\"exp_month\\\": 9,\\n \\\"exp_year\\\": 2024,\\n \\\"fingerprint\\\": \\\"hfaVNMiXc0dYSiC5\\\",\\n \\\"last4\\\": \\\"4242\\\",\\n \\\"tokenization_method\\\": \\\"apple_pay\\\",\\n \\\"used\\\": false\\n },\\n \\\"network_transaction_id\\\": \\\"104102978678771\\\",\\n \\\"overcapture\\\": {\\n \\\"maximum_amount_capturable\\\": 50,\\n \\\"status\\\": \\\"unavailable\\\"\\n },\\n \\\"three_d_secure\\\": null,\\n \\\"wallet\\\": {\\n \\\"apple_pay\\\": {\\n \\\"type\\\": \\\"apple_pay\\\"\\n },\\n \\\"dynamic_last4\\\": \\\"4242\\\",\\n \\\"type\\\": \\\"apple_pay\\\"\\n }\\n },\\n \\\"type\\\": \\\"card\\\"\\n },\\n \\\"radar_options\\\": {},\\n \\\"receipt_email\\\": null,\\n \\\"receipt_number\\\": null,\\n \\\"receipt_url\\\": \\\"https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xNjBEWDZBV090Z295c29nKPu_tbAGMgb1i-5uogg6LBYtHz5nv48TLnQFKbUhbQOjDLetYGrcnmnG64XzKTY69nso826Kd0cANL-w\\\",\\n \\\"refunded\\\": false,\\n \\\"refunds\\\": {\\n \\\"object\\\": \\\"list\\\",\\n \\\"data\\\": [],\\n \\\"has_more\\\": false,\\n \\\"total_count\\\": 0,\\n \\\"url\\\": \\\"/v1/charges/ch_3P1UIQAWOtgoysog2zDy9BAh/refunds\\\"\\n },\\n \\\"review\\\": null,\\n \\\"shipping\\\": null,\\n \\\"source\\\": null,\\n \\\"source_transfer\\\": null,\\n \\\"statement_descriptor\\\": null,\\n \\\"statement_descriptor_suffix\\\": null,\\n \\\"status\\\": \\\"succeeded\\\",\\n \\\"transfer_data\\\": null,\\n \\\"transfer_group\\\": null\\n }\\n ],\\n \\\"has_more\\\": false,\\n \\\"total_count\\\": 1,\\n \\\"url\\\": \\\"/v1/charges?payment_intent=pi_3P1UIQAWOtgoysog22LYv5Ie\\\"\\n },\\n \\\"client_secret\\\": \\\"pi_3P1UIQAWOtgoysog22LYv5Ie_secret_BXrSnt0ALWlIKXABbi8BoFJm0\\\",\\n \\\"confirmation_method\\\": \\\"automatic\\\",\\n \\\"created\\\": 1712152570,\\n \\\"currency\\\": \\\"usd\\\",\\n \\\"customer\\\": null,\\n \\\"description\\\": null,\\n \\\"invoice\\\": null,\\n \\\"last_payment_error\\\": null,\\n \\\"latest_charge\\\": \\\"ch_3P1UIQAWOtgoysog2zDy9BAh\\\",\\n \\\"level3\\\": null,\\n \\\"livemode\\\": false,\\n \\\"metadata\\\": {\\n \\\"connect_agent\\\": \\\"placeholder\\\",\\n \\\"order_id\\\": \\\"9900a089-9ce6-4158-9605-10b5633d1d57\\\",\\n \\\"transaction_token\\\": \\\"WmaAqGg0LW0ahLEvwIkMMCAKHKe\\\"\\n },\\n \\\"next_action\\\": null,\\n \\\"on_behalf_of\\\": null,\\n \\\"payment_method\\\": \\\"pm_1P1UIQAWOtgoysogLERqyfg0\\\",\\n \\\"payment_method_configuration_details\\\": null,\\n \\\"payment_method_options\\\": {\\n \\\"card\\\": {\\n \\\"installments\\\": null,\\n \\\"mandate_options\\\": null,\\n \\\"network\\\": null,\\n \\\"request_three_d_secure\\\": \\\"automatic\\\"\\n }\\n },\\n \\\"payment_method_types\\\": [\\n \\\"card\\\"\\n ],\\n \\\"processing\\\": null,\\n \\\"receipt_email\\\": null,\\n \\\"review\\\": null,\\n \\\"setup_future_usage\\\": null,\\n \\\"shipping\\\": null,\\n \\\"source\\\": null,\\n \\\"statement_descriptor\\\": null,\\n \\\"statement_descriptor_suffix\\\": null,\\n \\\"status\\\": \\\"succeeded\\\",\\n \\\"transfer_data\\\": null,\\n \\\"transfer_group\\\": null\\n}\" + read 6581 bytes + Conn close\n" + SCRUBBED + end + + def successful_purchase_avs_pass + <<-RESPONSE + { + "id": "pi_3OAbBTAWOtgoysog36MuKzzw", + "object": "payment_intent", + "amount": 2000, + "amount_capturable": 0, + "amount_received": 2000, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [ + { + "id": "ch_3OAbBTAWOtgoysog3eoQxrT9", + "object": "charge", + "amount": 2000, + "amount_captured": 2000, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 37, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_3OAbBTAWOtgoysog36MuKzzw", + "payment_method": "pm_1OAbBTAWOtgoysogVf7KTk4H", + "payment_method_details": { + "card": { + "amount_authorized": 2000, + "brand": "visa", + "checks": { + "address_line1_check": "pass", + "address_postal_code_check": "pass", + "cvc_check": "pass" + }, + "country": "US", + "ds_transaction_id": null, + "exp_month": 10, + "exp_year": 2028, + "extended_authorization": { + "status": "disabled" + }, + "fingerprint": "hfaVNMiXc0dYSiC5", + "funding": "credit", + "incremental_authorization": { + "status": "unavailable" + }, + "installments": null, + "last4": "4242", + "mandate": null, + "moto": null, + "multicapture": { + "status": "unavailable" + }, + "network": "visa", + "network_token": { + "used": false + }, + "network_transaction_id": "104102978678771", + "overcapture": { + "maximum_amount_capturable": 2000, + "status": "unavailable" + }, + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xNjBEWDZBV090Z295c29nKJCUtKoGMgYHwo4IbXs6LBbLMStawAC9eTsIUAmLDXw4dZNPmxzC6ds3zZxb-WVIVBJi_F4M59cPA3fR", + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_3OAbBTAWOtgoysog3eoQxrT9/refunds" + } + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_3OAbBTAWOtgoysog36MuKzzw" + }, + "client_secret": "pi_3OAbBTAWOtgoysog36MuKzzw_secret_YjUUEVStFrCFJK0imrUjspILY", + "confirmation_method": "automatic", + "created": 1699547663, + "currency": "usd", + "latest_charge": "ch_3OAbBTAWOtgoysog3eoQxrT9", + "payment_method": "pm_1OAbBTAWOtgoysogVf7KTk4H", + "payment_method_types": [ + "card" + ], + "status": "succeeded" + } + RESPONSE + end end diff --git a/test/unit/gateways/stripe_test.rb b/test/unit/gateways/stripe_test.rb index ea600f5fab3..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') @@ -876,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, {}) @@ -959,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, {}) @@ -1254,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) @@ -1433,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 @@ -1453,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 @@ -1474,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 @@ -1494,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..59703d1e9a3 --- /dev/null +++ b/test/unit/gateways/sum_up_test.rb @@ -0,0 +1,436 @@ +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_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, true, 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_errors + responses = @gateway.send(:parse, failed_complete_checkout_array_response) + error_code = @gateway.send(:format_errors, responses) + assert_equal format_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, false, 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" + }, + { + "message": "Validation error", + "param": "card", + "error_code": "The value located under the \'$.card.number\' path is not a valid card number" + } + ] + RESPONSE + end + + def failed_refund_response + <<-RESPONSE + { + "message": "The transaction is not refundable in its current state", + "error_code": "CONFLICT" + } + RESPONSE + end + + def format_errors_response + { + error_code: 'MULTIPLE_INVALID_PARAMETERS', + message: 'Validation error', + errors: [{ error_code: 'The card is expired', param: 'card' }, { error_code: "The value located under the '$.card.number' path is not a valid card number", 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/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/vantiv_express_test.rb b/test/unit/gateways/vantiv_express_test.rb new file mode 100644 index 00000000000..642ed7bfd19 --- /dev/null +++ b/test/unit/gateways/vantiv_express_test.rb @@ -0,0 +1,651 @@ +require 'test_helper' + +class VantivExpressTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = VantivExpressGateway.new(fixtures(:element)) + @credit_card = credit_card + @check = check + @amount = 100 + + @options = { + billing_address: address, + description: 'Store Purchase' + } + + @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 + ) + + @google_pay_network_token = network_tokenization_credit_card( + '6011000400000000', + 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_amex = network_tokenization_credit_card( + '34343434343434', + month: '01', + year: Time.new.year + 2, + first_name: 'Jane', + last_name: 'Doe', + verification_value: '7373', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + transaction_id: '123456789', + source: :apple_pay, + brand: 'american_express' + ) + 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 '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) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + end + + def test_successful_purchase_with_echeck + @gateway.expects(:ssl_post).returns(successful_purchase_with_echeck_response) + + response = @gateway.purchase(@amount, @check, @options) + assert_success response + + assert_equal '2005838412|100', response.authorization + end + + def test_failed_purchase_with_echeck + @gateway.expects(:ssl_post).returns(failed_purchase_with_echeck_response) + + response = @gateway.purchase(@amount, @check, @options) + assert_failure response + end + + def test_successful_purchase_with_apple_pay_visa_no_eci + @apple_pay_network_token.eci = nil + + response = stub_comms do + @gateway.purchase(@amount, @apple_pay_network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match '6', data + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_apple_pay_amex_no_eci + response = stub_comms do + @gateway.purchase(@amount, @apple_pay_amex, @options) + end.check_request do |_endpoint, data, _headers| + assert_match '9', data + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_apple_pay + response = stub_comms do + @gateway.purchase(@amount, @apple_pay_network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match '2', data + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_google_pay + response = stub_comms do + @gateway.purchase(@amount, @google_pay_network_token, @options) + end.check_request do |_endpoint, data, _headers| + assert_match '1', data + end.respond_with(successful_purchase_response) + + assert_success response + assert_equal 'Approved', response.message + end + + def test_successful_purchase_with_payment_account_token + @gateway.expects(:ssl_post).returns(successful_purchase_with_payment_account_token_response) + + response = @gateway.purchase(@amount, 'payment-account-token-id', @options) + assert_success response + + assert_equal '2005838405|100', response.authorization + end + + def test_failed_purchase_with_payment_account_token + @gateway.expects(:ssl_post).returns(failed_purchase_with_payment_account_token_response) + + response = @gateway.purchase(@amount, 'bad-payment-account-token-id', @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 'Approved', response.message + assert_equal '2005832533|100', 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_equal 'Declined', response.message + end + + def test_successful_capture + @gateway.expects(:ssl_post).returns(successful_capture_response) + + response = @gateway.capture(@amount, 'trans-id') + assert_success response + assert_equal 'Success', response.message + end + + def test_failed_capture + @gateway.expects(:ssl_post).returns(failed_capture_response) + + response = @gateway.capture(@amount, 'bad-trans-id') + assert_failure response + assert_equal 'TransactionID required', response.message + end + + def test_successful_refund + @gateway.expects(:ssl_post).returns(successful_refund_response) + + response = @gateway.refund(@amount, 'trans-id') + assert_success response + assert_equal 'Approved', response.message + end + + def test_failed_refund + @gateway.expects(:ssl_post).returns(failed_refund_response) + + response = @gateway.refund(@amount, 'bad-trans-id') + assert_failure response + assert_equal 'TransactionID required', response.message + end + + def test_successful_void + @gateway.expects(:ssl_post).returns(successful_void_response) + + response = @gateway.void('trans-id') + assert_success response + assert_equal 'Success', response.message + end + + def test_failed_void + @gateway.expects(:ssl_post).returns(failed_void_response) + + response = @gateway.void('bad-trans-id') + assert_failure response + assert_equal 'TransactionAmount required', response.message + end + + def test_successful_verify + @gateway.expects(:ssl_post).returns(successful_verify_response) + + response = @gateway.verify(@credit_card, @options) + assert_success response + end + + def test_handles_error_response + @gateway.expects(:ssl_post).returns(error_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_equal response.message, 'TargetNamespace required' + assert_failure response + end + + def test_successful_purchase_with_card_present_code + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(card_present_code: 'Present')) + end.check_request do |_endpoint, data, _headers| + assert_match '2', data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_element_string_lodging_fields + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(lodging: lodging_fields)) + end.check_request do |_endpoint, data, _headers| + assert_match "#{lodging_fields[:agreement_number]}", data + assert_match "#{lodging_fields[:check_in_date]}", data + assert_match "#{lodging_fields[:check_out_date]}", data + assert_match "#{lodging_fields[:room_amount]}", data + assert_match "#{lodging_fields[:room_tax]}", data + assert_match "#{lodging_fields[:no_show_indicator]}", data + assert_match "#{lodging_fields[:duration]}", data + assert_match "#{lodging_fields[:customer_name]}", data + assert_match "#{lodging_fields[:client_code]}", data + assert_match "#{lodging_fields[:extra_charges_detail]}", data + assert_match "#{lodging_fields[:extra_charges_amounts]}", data + assert_match '1', data + assert_match '3', data + assert_match '1', data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_element_enum_lodging_fields + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(lodging: enum_lodging_fields)) + end.check_request do |_endpoint, data, _headers| + assert_match '1', data + assert_match '3', data + assert_match '1', data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_element_string_terminal_fields + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(terminal_fields)) + end.check_request do |_endpoint, data, _headers| + assert_match '02', data + assert_match '0', data + assert_match '1', data + assert_match '0', data + assert_match '4', data + assert_match '3', data + assert_match '3', data + assert_match '2', data + assert_match '7', data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_enum_terminal_fields + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(enum_terminal_fields)) + end.check_request do |_endpoint, data, _headers| + assert_match '02', data + assert_match '0', data + assert_match '0', data + assert_match '0', data + assert_match '4', data + assert_match '3', data + assert_match '3', data + assert_match '2', data + assert_match '7', data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_payment_type + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(payment_type: 'NotUsed')) + end.check_request do |_endpoint, data, _headers| + assert_match '0', data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_submission_type + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(submission_type: 'NotUsed')) + end.check_request do |_endpoint, data, _headers| + assert_match '0', data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_duplicate_check_disable_flag + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_check_disable_flag: true)) + end.check_request do |_endpoint, data, _headers| + assert_match '1', data + end.respond_with(successful_purchase_response) + + assert_success response + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_check_disable_flag: 'true')) + end.check_request do |_endpoint, data, _headers| + assert_match '1', data + end.respond_with(successful_purchase_response) + + assert_success response + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_check_disable_flag: false)) + end.check_request do |_endpoint, data, _headers| + assert_not_match 'False', data + end.respond_with(successful_purchase_response) + + assert_success response + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_check_disable_flag: 'xxx')) + end.check_request do |_endpoint, data, _headers| + assert_not_match 'False', data + end.respond_with(successful_purchase_response) + + assert_success response + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_check_disable_flag: 'False')) + end.check_request do |_endpoint, data, _headers| + assert_not_match 'False', data + end.respond_with(successful_purchase_response) + + assert_success response + + # when duplicate_check_disable_flag is NOT passed, should not be in XML at all + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_not_match %r(False), data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_duplicate_override_flag + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_override_flag: true)) + end.check_request do |_endpoint, data, _headers| + assert_match '1', data + end.respond_with(successful_purchase_response) + + assert_success response + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_override_flag: 'true')) + end.check_request do |_endpoint, data, _headers| + assert_match '1', data + end.respond_with(successful_purchase_response) + + assert_success response + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_override_flag: false)) + end.check_request do |_endpoint, data, _headers| + assert_not_match 'False', data + end.respond_with(successful_purchase_response) + + assert_success response + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_override_flag: 'xxx')) + end.check_request do |_endpoint, data, _headers| + assert_not_match 'False', data + end.respond_with(successful_purchase_response) + + assert_success response + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(duplicate_override_flag: 'False')) + end.check_request do |_endpoint, data, _headers| + assert_not_match 'False', data + end.respond_with(successful_purchase_response) + + assert_success response + + # when duplicate_override_flag is NOT passed, should not be in XML at all + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.check_request do |_endpoint, data, _headers| + assert_not_match %r(False), data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_terminal_id + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(terminal_id: '02')) + end.check_request do |_endpoint, data, _headers| + assert_match '02', data + end.respond_with(successful_purchase_response) + + 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 + @gateway.credit(@amount, @credit_card, credit_options) + end.check_request do |_endpoint, data, _headers| + assert_match '14', data + assert_match '123', data + end.respond_with(successful_credit_response) + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def lodging_fields + { + agreement_number: '5a43d41dc251949cc3395542', + check_in_date: 20250910, + check_out_date: 20250915, + room_amount: 1000, + room_tax: 0, + no_show_indicator: 0, + duration: 5, + customer_name: 'francois dubois', + client_code: 'Default', + extra_charges_detail: '01', + extra_charges_amounts: 'Default', + prestigious_property_code: 'DollarLimit500', + special_program_code: 'AdvanceDeposit', + charge_type: 'Restaurant' + } + end + + def enum_lodging_fields + { + prestigious_property_code: 1, + special_program_code: 3, + charge_type: 1 + } + end + + def terminal_fields + { + terminal_id: '02', + terminal_type: 'Unknown', + card_present_code: 'Unknown', + card_holder_present_code: 'Default', + card_input_code: 'ManualKeyed', + cvv_presence_code: 'Illegible', + terminal_capability_code: 'MagstripeReader', + terminal_environment_code: 'LocalAttended' + } + end + + def enum_terminal_fields + { + terminal_id: '02', + terminal_type: 0, + card_present_code: 0, + card_holder_present_code: 0, + card_input_code: 4, + cvv_presence_code: 3, + terminal_capability_code: 3, + terminal_environment_code: 2 + } + end + + def pre_scrubbed + <<~XML + \n\n \n 1013963\n 683EED8A1A357EB91575A168E74482A74836FD72B1AD11B41B29B473CA9D65B9FE067701\n 3928907\n \n \n 5211\n Spreedly\n 1\n \n \n 4000100011112224\n 09\n 16\n Longbob Longsen\n 123\n \n \n 1.00\n Default\n \n \n 01\n UseDefault\n UseDefault\n UseDefault\n UseDefault\n UseDefault\n UseDefault\n UseDefault\n \n
\n 456 My Street\n Apt 1\n Ottawa\n ON\n K1C2N6\n
\n
+ XML + end + + def post_scrubbed + <<~XML + \n\n \n 1013963\n [FILTERED]\n 3928907\n \n \n 5211\n Spreedly\n 1\n \n \n [FILTERED]\n 09\n 16\n Longbob Longsen\n [FILTERED]\n \n \n 1.00\n Default\n \n \n 01\n UseDefault\n UseDefault\n UseDefault\n UseDefault\n UseDefault\n UseDefault\n UseDefault\n \n
\n 456 My Street\n Apt 1\n Ottawa\n ON\n K1C2N6\n
\n
+ XML + end + + def error_response + <<~XML + 103TargetNamespace required + XML + end + + def successful_purchase_response + <<~XML + 0Approved20151201104518UTC-05:00000APRegularTotals1962962.00FullBatchCurrentDefaultNMVisa2005831886000045SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTApproved1False1.00DefaultUnknownCreditCardNullNull
456 My StreetK1C2N6
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull
+ XML + end + + def successful_purchase_with_echeck_response + <<~XML + 0Success20151202090320UTC-05:000Transaction ProcessedRegularTotalsFullBatchCurrentDefault2005838412347520966b3df3e93051b5dc85c355a54e3012c2SystemDefaultFalseFalseFalseFalseNULL_PROCESSOR_TESTPending10FalseDefaultUnknownCreditCardNullNull
456 My StreetK1C2N6
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull
+ XML + end + + def successful_purchase_with_payment_account_token_response + <<~XML + 0Approved20151202090144UTC-05:00000APRegularTotals11552995.00FullBatchCurrentDefaultNVisa2005838405000001c0d498aa3c2c07169d13a989a7af91af5bc4e6a0SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTApproved1False1.00DefaultUnknownC875D86C-5913-487D-822E-76B27E2C2A4ECreditCard147b0b90f74faac13afb618fdabee3a4e75bf03bNullNull
456 My StreetK1C2N6
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull
+ XML + end + + def successful_credit_response + <<~XML + 0Approved20211122174635UTC-06:00000APRegularTotals1102103.00FullBatchCurrentVisa4000101228162530000461SystemDefaultFalseFalseFalseFalseNULL_PROCESSOR_TESTApproved1FalseDefaultUnknownNotUsedNotUsedCreditCardNullNull
OneTimeFutureFalseActivePersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefault + XML + end + + def failed_purchase_with_echeck_response + <<~XML + 101CardNumber Required20151202090342UTC-05:00RegularTotals1FullBatchCurrentDefault8fe3b762a2a4344d938c32be31f36e354fb28ee3SystemDefaultFalseFalseFalseFalseNULL_PROCESSOR_TESTFalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull + XML + end + + def failed_purchase_with_payment_account_token_response + <<~XML + 103PAYMENT ACCOUNT NOT FOUND20151202090245UTC-05:00RegularTotals1FullBatchCurrentDefault564bd4943761a37bdbb3f201faa56faa091781b5SystemDefaultFalseFalseFalseFalseNULL_PROCESSOR_TESTFalseDefaultUnknownasdfCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull + XML + end + + def failed_purchase_response + <<~XML + 20Declined20151201104817UTC-05:00007DECLINEDRegularTotals1FullBatchCurrentDefaultNMVisa2005831909SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTDeclined2FalseDefaultUnknownCreditCardNullNull
456 My StreetK1C2N6
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull
+ XML + end + + def successful_authorize_response + <<~XML + 0Approved20151201120220UTC-05:00000APRegularTotals1FullBatchCurrentDefaultNMVisa2005832533000002SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTAuthorized5False1.00DefaultUnknownCreditCardNullNull
456 My StreetK1C2N6
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull
+ XML + end + + def failed_authorize_response + <<~XML + 20Declined20151201120315UTC-05:00007DECLINEDRegularTotals1FullBatchCurrentDefaultNMVisa2005832537SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTDeclined2FalseDefaultUnknownCreditCardNullNull
456 My StreetK1C2N6
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull
+ XML + end + + def successful_capture_response + <<~XML + 0Success20151201120222UTC-05:00000APRegularTotals1972963.00FullBatchCurrentDefaultVisa2005832535000002SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTApproved1FalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull + XML + end + + def failed_capture_response + <<~XML + 101TransactionID requiredUTC-05:00RegularTotalsFullBatchCurrentDefaultSystemDefaultFalseFalseFalseFalseFalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull + XML + end + + def successful_refund_response + <<~XML + 0Approved20151201120437UTC-05:00000APRegularTotals1992963.00FullBatchCurrentDefaultVisa2005832540000004SystemDefaultFalseFalseFalseFalseNULL_PROCESSOR_TESTApproved1FalseDefaultUnknownCreditCardNullNull
456 My StreetK1C2N6
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull
+ XML + end + + def failed_refund_response + <<~XML + 101TransactionID requiredUTC-05:00RegularTotalsFullBatchCurrentDefaultSystemDefaultFalseFalseFalseFalseFalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull + XML + end + + def successful_void_response + <<~XML + 0Success20151201120516UTC-05:00006REVERSEDRegularTotalsFullBatchCurrentDefaultVisa2005832551000005SystemDefaultaVb001234567810425c0425d5e00FalseFalseFalseFalseNULL_PROCESSOR_TESTSuccess8FalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull + XML + end + + def failed_void_response + <<~XML + 101TransactionAmount requiredUTC-05:00RegularTotalsFullBatchCurrentDefaultSystemDefaultFalseFalseFalseFalseFalseDefaultUnknownCreditCardNullNull
OneTimeFutureFalseActiveCheckingPersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefaultNull + XML + end + + def successful_verify_response + <<~XML + 0Success20200505094556UTC-05:00000APRegularTotalsFullBatchCurrentNVisa400010481381541SystemDefaultFalseFalseFalseFalseNULL_PROCESSOR_TESTSuccess8FalseDefaultUnknownNotUsedNotUsedCreditCardNullNull
456 My StreetK1C2N6
OneTimeFutureFalseActivePersonalNullNullFalseFalseFalseNotUsedUnknownUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultUseDefaultRegularNotUsedDefaultUnusedUnusedNoAdjustmentsFalseNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNotSpecifiedLedgerBalancePositiveNonParticipantDefaultDefault
+ XML + end +end diff --git a/test/unit/gateways/visanet_peru_test.rb b/test/unit/gateways/visanet_peru_test.rb index 81186a72ad9..fdbfc995b4a 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 @@ -20,10 +21,11 @@ def setup def test_successful_purchase @gateway.expects(:ssl_request).with(:post, any_parameters).returns(successful_authorize_response) @gateway.expects(:ssl_request).with(:put, any_parameters).returns(successful_capture_response) - response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response assert_equal 'OK', response.message + assert_not_nil response.params['purchaseNumber'] assert_match %r([0-9]{9}|$), response.authorization assert_equal 'de9dc65c094fb4f1defddc562731af81', response.params['externalTransactionId'] @@ -40,26 +42,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/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 81a7d4683d5..883b69acb20 100644 --- a/test/unit/gateways/wompi_test.rb +++ b/test/unit/gateways/wompi_test.rb @@ -148,6 +148,20 @@ def test_failed_void assert_equal 'La entidad solicitada no existe', response.message end + def test_successful_purchase_with_tip_in_cents + response = stub_comms(@gateway) do + @gateway.purchase(@amount, @credit_card, @options.merge(tip_in_cents: 300)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['tip_in_cents'], 300 + assert_match @amount.to_s, data + end.respond_with(successful_purchase_response) + assert_success response + + assert_equal '113879-1635300853-71494', response.authorization + assert response.test? + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed diff --git a/test/unit/gateways/worldpay_test.rb b/test/unit/gateways/worldpay_test.rb index 98a1e5dde3e..9a12ae35728 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,32 +55,41 @@ 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' + ) + + @google_pay_network_token_without_eci = network_tokenization_credit_card( + '4444333322221111', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: Time.new.year + 2, + source: :google_pay, + transaction_id: '123456789' + ) @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' - }, + tax_amount: '20', ship_from_postal_code: '43245', destination_postal_code: '54545', destination_country_code: 'CO', @@ -80,8 +97,7 @@ def setup day_of_month: Date.today.day, month: Date.today.month, year: Date.today.year - }, - tax_exempt: 'false' + } } } @@ -89,67 +105,95 @@ def setup 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: { + tax_amount: '20', + discount_amount: '1', + shipping_amount: '50', + duty_amount: '20', + line_items: [{ description: 'Laptop 14', product_code: 'LP00125', commodity_code: 'COM00125', quantity: '2', - unit_cost: { - amount: '1500', - exponent: '2', - currency: 'USD' - }, + unit_cost: '1500', 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' - } - } + item_discount_amount: '200', + discount_amount: '0', + tax_amount: '500', + total_amount: '4000' + }, + { + description: 'Laptop 15', + product_code: 'LP00120', + commodity_code: 'COM00125', + quantity: '2', + unit_cost: '1000', + unit_of_measure: 'each', + item_discount_amount: '200', + tax_amount: '500', + discount_amount: '0', + total_amount: '3000' + }] } } 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 response = stub_comms do @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 @@ -266,6 +310,73 @@ def test_authorize_passes_stored_credential_options assert_success response end + def test_authorize_with_nt_passes_stored_credential_options + options = @options.merge( + stored_credential_usage: 'USED', + stored_credential_initiated_reason: 'UNSCHEDULED', + stored_credential_transaction_id: '000000000000020005060720116005060' + ) + response = stub_comms do + @gateway.authorize(@amount, @nt_credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(//, data) + assert_match(/000000000000020005060720116005060\<\/schemeTransactionIdentifier\>/, data) + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_nt_passes_standard_stored_credential_options + stored_credential_params = stored_credential(:used, :unscheduled, :merchant, network_transaction_id: 20_005_060_720_116_005_060) + response = stub_comms do + @gateway.authorize(@amount, @nt_credit_card, @options.merge({ stored_credential: stored_credential_params })) + end.check_request do |_endpoint, data, _headers| + assert_match(//, data) + assert_match(/20005060720116005060\<\/schemeTransactionIdentifier\>/, data) + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_passes_correct_stored_credential_options_for_first_recurring + options = @options.merge( + stored_credential_usage: 'FIRST', + stored_credential_initiated_reason: 'RECURRING' + ) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(//, data) + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_passes_correct_stored_credential_options_for_used_recurring + options = @options.merge( + stored_credential_usage: 'USED', + stored_credential_initiated_reason: 'RECURRING', + stored_credential_transaction_id: '000000000000020005060720116005061' + ) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(//, data) + assert_match(/000000000000020005060720116005061\<\/schemeTransactionIdentifier\>/, data) + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_passes_correct_stored_credentials_for_first_installment + options = @options.merge( + stored_credential_usage: 'FIRST', + stored_credential_initiated_reason: 'INSTALMENT' + ) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(//, data) + end.respond_with(successful_authorize_response) + assert_success response + end + def test_authorize_passes_sub_merchant_data options = @options.merge(@sub_merchant_options) response = stub_comms do @@ -302,12 +413,30 @@ def test_transaction_with_level_two_data assert_match %r(INV12233565), data assert_match %r(CUST00000101), data assert_match %r(VAT1999292), data - assert_match %r(), data.gsub(/\s+/, '') + 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_two_data_without_tax + @level_two_data[:level_2_data][:tax_amount] = 0 + 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(true), data assert_match %r(), data.gsub(/\s+/, '') end.respond_with(successful_authorize_response) assert_success response @@ -320,11 +449,11 @@ def test_transaction_with_level_three_data 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+/, '') + 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(Laptop14LP00125COM001252eachLaptop15LP00120COM001252each), data.gsub(/\s+/, '') end.respond_with(successful_authorize_response) assert_success response end @@ -361,15 +490,6 @@ def test_successful_authorize_with_network_token_with_eci assert_success response end - def test_successful_authorize_with_network_token_without_eci - response = stub_comms do - @gateway.authorize(@amount, @nt_credit_card_without_eci, @options) - end.check_request do |_endpoint, data, _headers| - assert_match %r(07), data - end.respond_with(successful_authorize_response) - assert_success response - end - def test_successful_purchase_with_elo response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'BRL')) @@ -612,9 +732,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 @@ -623,9 +741,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 @@ -633,17 +749,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 @@ -1041,9 +1153,11 @@ def test_3ds_name_not_coerced_in_production def test_3ds_additional_information browser_size = '390x400' session_id = '0215ui8ib1' + df_reference_id = '1326vj9jc2' options = @options.merge( session_id: session_id, + df_reference_id: df_reference_id, browser_size: browser_size, execute_threed: true, three_ds_version: '2.0.1' @@ -1052,7 +1166,7 @@ def test_3ds_additional_information stub_comms do @gateway.authorize(@amount, @credit_card, options) end.check_request do |_endpoint, data, _headers| - assert_tag_with_attributes 'additional3DSData', { 'dfReferenceId' => session_id, 'challengeWindowSize' => browser_size }, data + assert_tag_with_attributes 'additional3DSData', { 'dfReferenceId' => df_reference_id, 'challengeWindowSize' => browser_size }, data end.respond_with(successful_authorize_response) end @@ -1122,7 +1236,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 @@ -1359,6 +1473,32 @@ def test_network_token_type_assignation_when_network_token def test_network_token_type_assignation_when_google_pay stub_comms do @gateway.authorize(@amount, @google_pay_network_token, @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match %r(), data + assert_match %r(05), data + end + end + + def test_google_pay_without_eci_value + stub_comms do + @gateway.authorize(@amount, @google_pay_network_token_without_eci, @options) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match %r(), data + end + end + + def test_google_pay_with_use_default_eci_value + stub_comms do + @gateway.authorize(@amount, @google_pay_network_token_without_eci, @options.merge({ use_default_eci: true })) + end.check_request(skip_response: true) do |_endpoint, data, _headers| + assert_match %r(), data + assert_match %r(07), data + end + end + + def test_network_token_type_assignation_when_google_pay_pan_only + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge!(wallet_type: :google_pay)) end.check_request(skip_response: true) do |_endpoint, data, _headers| assert_match %r(), data end @@ -1374,6 +1514,22 @@ def test_order_id_crop_and_clean assert_success response end + def test_authorize_prefers_options_for_ntid + stored_credential_params = stored_credential(:used, :recurring, :merchant, network_transaction_id: '3812908490218390214124') + options = @options.merge( + stored_credential_transaction_id: '000000000000020005060720116005060' + ) + + options.merge!({ stored_credential: stored_credential_params }) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(//, data) + assert_match(/000000000000020005060720116005060\<\/schemeTransactionIdentifier\>/, data) + end.respond_with(successful_authorize_response) + 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 }) @@ -2084,7 +2240,7 @@ def sample_authorization_request Products Products Products - + 4242424242424242 @@ -2104,7 +2260,7 @@ def sample_authorization_request (555)555-5555
- + @@ -2127,7 +2283,7 @@ def transcript Purchase - + 4111111111111111 @@ -2143,7 +2299,7 @@ def transcript US
- + wow@example.com @@ -2162,7 +2318,7 @@ def scrubbed_transcript Purchase - + [FILTERED] @@ -2178,7 +2334,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..40eab0f5d01 --- /dev/null +++ b/test/unit/gateways/xpay_test.rb @@ -0,0 +1,231 @@ +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 = { + order_id: 'ngGFbpHStk', + order: { + currency: 'EUR', + amount: @amount, + customer_info: { + card_holder_name: 'Ryan Reynolds', + card_holder_email: nil, + billing_address: address + } + } + } + @server_error = stub(code: 500, message: 'Internal Server Error', body: 'failure') + @uuid_regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ + 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/3steps/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_for_orders + stub_comms(@gateway, :ssl_post) do + @gateway.preauth(@amount, @credit_card, @options) + end.check_request do |_endpoint, _data, headers| + assert_equal headers['Content-Type'], 'application/json' + assert_equal headers['X-Api-Key'], 'some api key' + assert_true @uuid_regex.match?(headers['Correlation-Id'].to_s.downcase) + end.respond_with(successful_preauth_response) + end + + def test_check_request_headers_for_operations + stub_comms(@gateway, :ssl_post) do + @gateway.capture(@amount, '5e971065-e36a-430d-92e7-716efe515a6d#123', @options) + end.check_request do |_endpoint, _data, headers| + assert_equal headers['Content-Type'], 'application/json' + assert_equal headers['X-Api-Key'], 'some api key' + assert_true @uuid_regex.match?(headers['Correlation-Id'].to_s.downcase) + assert_true @uuid_regex.match?(headers['Idempotency-Key'].to_s.downcase) + end.respond_with(successful_capture_response) + end + + def test_check_preauth_endpoint + stub_comms(@gateway, :ssl_post) do + @gateway.preauth(@amount, @credit_card, @options) + end.check_request do |endpoint, _data| + assert_match(/orders\/3steps\/init/, endpoint) + end.respond_with(successful_preauth_response) + end + + def test_check_authorize_endpoint + @gateway.expects(:ssl_post).times(2).returns(successful_validation_response, successful_authorize_response) + @options[:correlation_id] = 'bb34f2b1-a4ed-4054-a29f-2b908068a17e' + assert response = @gateway.authorize(@amount, @credit_card, @options) + assert_instance_of MultiResponse, response + assert_success response + + assert_equal 'bb34f2b1-a4ed-4054-a29f-2b908068a17e#592398610041040779', response.authorization + assert_equal 'AUTHORIZED', response.message + assert response.test? + end + + def test_check_purchase_endpoint + @options[:correlation_id] = 'bb34f2b1-a4ed-4054-a29f-2b908068a17e' + @gateway.expects(:ssl_post).times(2).returns(successful_validation_response, successful_purchase_response) + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_instance_of MultiResponse, response + assert_success response + + assert_equal 'bb34f2b1-a4ed-4054-a29f-2b908068a17e#249959437570040779', response.authorization + assert_equal 'EXECUTED', response.message + assert response.test? + end + + def test_internal_server_error + ActiveMerchant::Connection.any_instance.expects(:request).returns(@server_error) + response = @gateway.preauth(@amount, @credit_card, @options) + assert_equal response.error_code, 500 + assert_equal response.message, 'failure' + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def successful_preauth_response + <<-RESPONSE + { + "operation":{ + "orderId":"OpkGYfLLkYAiqzyxUNkvpB1WB4e", + "operationId":"696995050267340689", + "channel":null, + "operationType":"AUTHORIZATION", + "operationResult":"PENDING", + "operationTime":"2024-03-08 05:22:36.277", + "paymentMethod":"CARD", + "paymentCircuit":"VISA", + "paymentInstrumentInfo":"***4549", + "paymentEndToEndId":"696995050267340689", + "cancelledOperationId":null, + "operationAmount":"100", + "operationCurrency":"EUR", + "customerInfo":{ + "cardHolderName":"Amee Kuhlman", + "cardHolderEmail":null, + "billingAddress":null, + "shippingAddress":null, + "mobilePhoneCountryCode":null, + "mobilePhone":null, + "homePhone":null, + "workPhone":null, + "cardHolderAcctInfo":null, + "merchantRiskIndicator":null + }, + "warnings":[], + "paymentLinkId":null, + "omnichannelId":null, + "additionalData":{ + "maskedPan":"434994******4549", + "cardId":"952fd84b4562026c9f35345599e1f043d893df720b914619b55d682e7435e13d", "cardId4":"B8PJeZ8PQ+/eWfkqJeZr1HDc7wFaS9sbxVOYwBRC9Ro=", + "cardExpiryDate":"202605" + } + }, + "threeDSEnrollmentStatus":"ENROLLED", + "threeDSAuthRequest":"notneeded", + "threeDSAuthUrl":"https://stg-ta.nexigroup.com/monetaweb/phoenixstos" + } + RESPONSE + end + + def successful_validation_response + <<-RESPONSE + {"operation":{"additionalData":{"maskedPan":"434994******4549","cardId":"952fd84b4562026c9f35345599e1f043d893df720b914619b55d682e7435e13d","cardId4":"B8PJeZ8PQ+/eWfkqJeZr1HDc7wFaS9sbxVOYwBRC9Ro=","cardExpiryDate":"202612"},"channelDetail":"SERVER_TO_SERVER","customerInfo":{"cardHolderEmail":"Rosalia_VonRueden@gmail.com","cardHolderName":"Walter Mante"},"operationAmount":"100","operationCurrency":"978","operationId":"592398610041040779","operationResult":"THREEDS_VALIDATED","operationTime":"2024-03-17 03:10:21.152","operationType":"AUTHORIZATION","orderId":"304","paymentCircuit":"VISA","paymentEndToEndId":"592398610041040779","paymentInstrumentInfo":"***4549","paymentMethod":"CARD","warnings":[{"code":"003","description":"Warning - BillingAddress: field country code is not valid, the size must be 3 - BillingAddress has not been considered."},{"code":"007","description":"Warning - BillingAddress: field Province code is not valid, the size must be between 1 and 2 - BillingAddress has not been considered."},{"code":"010","description":"Warning - ShippingAddress: field country code is not valid, the size must be 3 - ShippingAddress has not been considered."},{"code":"014","description":"Warning - ShippingAddress: field Province code is not valid, the size must be between 1 and 2 - ShippingAddress has not been considered."}]},"threeDSAuthResult":{"authenticationValue":"AAcBBVYIEQAAAABkl4B3dQAAAAA=","cavvAlgorithm":"3","eci":"05","merchantAcquirerBin":"434495","xid":"S0JvQiFdWC16MzshPy1nMUVtOy8=","status":"VALIDATED","vendorcode":"","version":"2.2.0"}} + RESPONSE + end + + def successful_authorize_response + <<-RESPONSE + {"operation":{"additionalData":{"maskedPan":"434994******4549","authorizationCode":"123456","cardCountry":"380","cardId":"952fd84b4562026c9f35345599e1f043d893df720b914619b55d682e7435e13d","cardType":"MONETA","authorizationStatus":"000","cardId4":"B8PJeZ8PQ+/eWfkqJeZr1HDc7wFaS9sbxVOYwBRC9Ro=","cardExpiryDate":"202612","rrn":"914280154542","schemaTID":"144"},"channelDetail":"SERVER_TO_SERVER","customerInfo":{"cardHolderEmail":"Rosalia_VonRueden@gmail.com","cardHolderName":"Walter Mante"},"operationAmount":"100","operationCurrency":"978","operationId":"592398610041040779","operationResult":"AUTHORIZED","operationTime":"2024-03-17 03:10:23.106","operationType":"AUTHORIZATION","orderId":"304","paymentCircuit":"VISA","paymentEndToEndId":"592398610041040779","paymentInstrumentInfo":"***4549","paymentMethod":"CARD","warnings":[{"code":"003","description":"Warning - BillingAddress: field country code is not valid, the size must be 3 - BillingAddress has not been considered."},{"code":"007","description":"Warning - BillingAddress: field Province code is not valid, the size must be between 1 and 2 - BillingAddress has not been considered."},{"code":"010","description":"Warning - ShippingAddress: field country code is not valid, the size must be 3 - ShippingAddress has not been considered."},{"code":"014","description":"Warning - ShippingAddress: field Province code is not valid, the size must be between 1 and 2 - ShippingAddress has not been considered."}]}} + RESPONSE + end + + def successful_purchase_response + <<-RESPONSE + {"operation":{"additionalData":{"maskedPan":"434994******4549","authorizationCode":"123456","cardCountry":"380","cardId":"952fd84b4562026c9f35345599e1f043d893df720b914619b55d682e7435e13d","cardType":"MONETA","authorizationStatus":"000","cardId4":"B8PJeZ8PQ+/eWfkqJeZr1HDc7wFaS9sbxVOYwBRC9Ro=","cardExpiryDate":"202612","rrn":"914280154542","schemaTID":"144"},"channelDetail":"SERVER_TO_SERVER","customerInfo":{"cardHolderEmail":"Rosalia_VonRueden@gmail.com","cardHolderName":"Walter Mante"},"operationAmount":"90000","operationCurrency":"978","operationId":"249959437570040779","operationResult":"EXECUTED","operationTime":"2024-03-17 03:14:50.141","operationType":"AUTHORIZATION","orderId":"333","paymentCircuit":"VISA","paymentEndToEndId":"249959437570040779","paymentInstrumentInfo":"***4549","paymentMethod":"CARD","warnings":[{"code":"003","description":"Warning - BillingAddress: field country code is not valid, the size must be 3 - BillingAddress has not been considered."},{"code":"007","description":"Warning - BillingAddress: field Province code is not valid, the size must be between 1 and 2 - BillingAddress has not been considered."},{"code":"010","description":"Warning - ShippingAddress: field country code is not valid, the size must be 3 - ShippingAddress has not been considered."},{"code":"014","description":"Warning - ShippingAddress: field Province code is not valid, the size must be between 1 and 2 - ShippingAddress has not been considered."}]}} + RESPONSE + end + + def successful_capture_response + <<-RESPONSE + {"operationId":"30762d01-931a-4083-b1c4-c829902056aa","operationTime":"2024-03-17 03:11:32.677"} + RESPONSE + end + + def pre_scrubbed + <<-PRE_SCRUBBED + opening connection to stg-ta.nexigroup.com:443... + opened + starting SSL for stg-ta.nexigroup.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_128_GCM_SHA256 + <- "POST /api/phoenix-0.0/psp/api/v1/orders/2steps/init HTTP/1.1\r\nContent-Type: application/json\r\nX-Api-Key: 5d952446-9004-4023-9eae-a527a152846b\r\nCorrelation-Id: ngGFbpHStk\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: stg-ta.nexigroup.com\r\nContent-Length: 268\r\n\r\n" + <- "{\"order\":{\"orderId\":\"ngGFbpHStk\",\"amount\":\"100\",\"currency\":\"EUR\",\"customerInfo\":{\"cardHolderName\":\"John Smith\"}},\"card\":{\"pan\":\"4349940199004549\",\"expiryDate\":\"0526\",\"cvv\":\"396\"},\"recurrence\":{\"action\":\"NO_RECURRING\"},\"exemptions\":\"NO_PREFERENCE\",\"threeDSAuthData\":{}}" + -> "HTTP/1.1 200 \r\n" + -> "cid: 2dd22695-c628-41d3-9c11-cdd6a72a59ec\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 970\r\n" + -> "Date: Tue, 28 Nov 2023 11:41:45 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 970 bytes... + -> "{\"operation\":{\"orderId\":\"ngGFbpHStk\",\"operationId\":\"829023675869933329\",\"channel\":null,\"operationType\":\"AUTHORIZATION\",\"operationResult\":\"PENDING\",\"operationTime\":\"2023-11-28 12:41:46.724\",\"paymentMethod\":\"CARD\",\"paymentCircuit\":\"VISA\",\"paymentInstrumentInfo\":\"***4549\",\"paymentEndToEndId\":\"829023675869933329\",\"cancelledOperationId\":null,\"operationAmount\":\"100\",\"operationCurrency\":\"EUR\",\"customerInfo\":{\"cardHolderName\":\"John Smith\",\"cardHolderEmail\":null,\"billingAddress\":null,\"shippingAddress\":null,\"mobilePhoneCountryCode\":null,\"mobilePhone\":null,\"homePhone\":null,\"workPhone\":null,\"cardHolderAcctInfo\":null,\"merchantRiskIndicator\":null},\"warnings\":[],\"paymentLinkId\":null,\"additionalData\":{\"maskedPan\":\"434994******4549\",\"cardId\":\"952fd84b4562026c9f35345599e1f043d893df720b914619b55d682e7435e13d\",\"cardExpiryDate\":\"202605\"}},\"threeDSEnrollmentStatus\":\"ENROLLED\",\"threeDSAuthRequest\":\"notneeded\",\"threeDSAuthUrl\":\"https://stg-ta.nexigroup.com/monetaweb/phoenixstos\"}" + read 970 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SCRUBBED + opening connection to stg-ta.nexigroup.com:443... + opened + starting SSL for stg-ta.nexigroup.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_128_GCM_SHA256 + <- "POST /api/phoenix-0.0/psp/api/v1/orders/2steps/init HTTP/1.1\r\nContent-Type: application/json\r\nX-Api-Key: [FILTERED]\r\nCorrelation-Id: ngGFbpHStk\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: stg-ta.nexigroup.com\r\nContent-Length: 268\r\n\r\n" + <- "{\"order\":{\"orderId\":\"ngGFbpHStk\",\"amount\":\"100\",\"currency\":\"EUR\",\"customerInfo\":{\"cardHolderName\":\"John Smith\"}},\"card\":{\"pan\":\"[FILTERED]\",\"expiryDate\":\"0526\",\"cvv\":\"[FILTERED]\"},\"recurrence\":{\"action\":\"NO_RECURRING\"},\"exemptions\":\"NO_PREFERENCE\",\"threeDSAuthData\":{}}" + -> "HTTP/1.1 200 \r\n" + -> "cid: 2dd22695-c628-41d3-9c11-cdd6a72a59ec\r\n" + -> "Content-Type: application/json\r\n" + -> "Content-Length: 970\r\n" + -> "Date: Tue, 28 Nov 2023 11:41:45 GMT\r\n" + -> "Connection: close\r\n" + -> "\r\n" + reading 970 bytes... + -> "{\"operation\":{\"orderId\":\"ngGFbpHStk\",\"operationId\":\"829023675869933329\",\"channel\":null,\"operationType\":\"AUTHORIZATION\",\"operationResult\":\"PENDING\",\"operationTime\":\"2023-11-28 12:41:46.724\",\"paymentMethod\":\"CARD\",\"paymentCircuit\":\"VISA\",\"paymentInstrumentInfo\":\"***4549\",\"paymentEndToEndId\":\"829023675869933329\",\"cancelledOperationId\":null,\"operationAmount\":\"100\",\"operationCurrency\":\"EUR\",\"customerInfo\":{\"cardHolderName\":\"John Smith\",\"cardHolderEmail\":null,\"billingAddress\":null,\"shippingAddress\":null,\"mobilePhoneCountryCode\":null,\"mobilePhone\":null,\"homePhone\":null,\"workPhone\":null,\"cardHolderAcctInfo\":null,\"merchantRiskIndicator\":null},\"warnings\":[],\"paymentLinkId\":null,\"additionalData\":{\"maskedPan\":\"434994******4549\",\"cardId\":\"952fd84b4562026c9f35345599e1f043d893df720b914619b55d682e7435e13d\",\"cardExpiryDate\":\"202605\"}},\"threeDSEnrollmentStatus\":\"ENROLLED\",\"threeDSAuthRequest\":\"notneeded\",\"threeDSAuthUrl\":\"https://stg-ta.nexigroup.com/monetaweb/phoenixstos\"}" + read 970 bytes + Conn close + POST_SCRUBBED + end +end diff --git a/test/unit/network_tokenization_credit_card_test.rb b/test/unit/network_tokenization_credit_card_test.rb index a10356c0c6b..a5c881cc0b6 100644 --- a/test/unit/network_tokenization_credit_card_test.rb +++ b/test/unit/network_tokenization_credit_card_test.rb @@ -6,7 +6,15 @@ def setup number: '4242424242424242', brand: 'visa', month: default_expiration_date.month, year: default_expiration_date.year, payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', eci: '05', - metadata: { device_manufacturer_id: '1324' } + metadata: { device_manufacturer_id: '1324' }, + payment_data: { + version: 'EC_v1', + data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9iOcBY4TjPZyNrnCwsJd2cq61bDQjo3agVU0LuEot2VIHHocVrp5jdy0FkxdFhGd+j7hPvutFYGwZPcuuBgROb0beA1wfGDi09I+OWL+8x5+8QPl+y8EAGJdWHXr4CuL7hEj4CjtUhfj5GYLMceUcvwgGaWY7WzqnEO9UwUowlDP9C3cD21cW8osn/IKROTInGcZB0mzM5bVHM73NSFiFepNL6rQtomp034C+p9mikB4nc+vR49oVop0Pf+uO7YVq7cIWrrpgMG7ussnc3u4bmr3JhCNtKZzRQ2MqTxKv/CfDq099JQIvTj8hbqswv1t+yQ5ZhJ3m4bcPwrcyIVej5J241R7dNPu9xVjM6LSOX9KeGZQGud', + signature: 'MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZKYr/0F+3ZD3VNoo6+8ZyBXkK3ifiY95tZn5jVQQ2PnenC/gIwMi3VRCGwowV3bF3zODuQZ/0XfCwhbZZPxnJpghJvVPh6fRuZy5sJiSFhBpkPCZIdAAAxggFfMIIBWwIBATCBhjB6MS4wLAYDVQQDDCVBcHBsZSBBcHBsaWNhdGlvbiBJbnRlZ3JhdGlvbiBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMCCCRD8qgGnfV3MA0GCWCGSAFlAwQCAQUAoGkwGAYkiG3j7AAAAAAAA', + header: { + ephemeralPublicKey: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQwjaSlnZ3EXpwKfWAd2e1VnbS6vmioMyF6bNcq/Qd65NLQsjrPatzHWbJzG7v5vJtAyrf6WhoNx3C1VchQxYuw==', transactionId: 'e220cc1504ec15835a375e9e8659e27dcbc1abe1f959a179d8308dd8211c9371", "publicKeyHash": "/4UKqrtx7AmlRvLatYt9LDt64IYo+G9eaqqS6LFOAdI=' + } + } ) @tokenized_apple_pay_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new( source: :apple_pay